From 7270fc1dba0639c1fbe58c3fd419043bdebc6d9c Mon Sep 17 00:00:00 2001 From: Jennie Alles Date: Mon, 22 Jul 2024 03:18:13 +0700 Subject: [PATCH 01/81] fix(components): add form ui --- package.json | 1 + pnpm-lock.yaml | 11 + ...eContainer.tsx => ActionPageContainer.tsx} | 13 +- .../components/AddNetworkHeader.tsx | 26 ++ .../components/AddNetworkStepper.tsx | 91 +++++ .../components/AddNetworkSubheader.tsx | 24 ++ .../add-network/components/FormWrapper.tsx | 53 +++ .../components/StepNavigationButtons.tsx | 59 +++ src/lib/pages/add-network/gas-fee-details.tsx | 256 ++++++++++++ src/lib/pages/add-network/index.tsx | 12 + src/lib/pages/add-network/network-details.tsx | 138 +++++++ .../pages/add-network/supported-features.tsx | 68 ++++ src/lib/pages/add-network/wallet-registry.tsx | 381 ++++++++++++++++++ src/lib/pages/admin/index.tsx | 6 +- src/lib/pages/deploy-script/index.tsx | 6 +- src/lib/pages/deploy/index.tsx | 6 +- src/lib/pages/faucet/index.tsx | 6 +- src/lib/pages/instantiate/completed.tsx | 6 +- src/lib/pages/instantiate/instantiate.tsx | 6 +- src/lib/pages/migrate/index.tsx | 6 +- src/lib/pages/publish-module/completed.tsx | 6 +- src/lib/pages/upload/completed.tsx | 6 +- src/lib/pages/upload/upload.tsx | 6 +- src/lib/styles/theme/components/checkbox.ts | 29 +- src/pages/[network]/add-network/index.tsx | 3 + src/pages/add-network/index.tsx | 3 + 26 files changed, 1188 insertions(+), 40 deletions(-) rename src/lib/components/{WasmPageContainer.tsx => ActionPageContainer.tsx} (62%) create mode 100644 src/lib/pages/add-network/components/AddNetworkHeader.tsx create mode 100644 src/lib/pages/add-network/components/AddNetworkStepper.tsx create mode 100644 src/lib/pages/add-network/components/AddNetworkSubheader.tsx create mode 100644 src/lib/pages/add-network/components/FormWrapper.tsx create mode 100644 src/lib/pages/add-network/components/StepNavigationButtons.tsx create mode 100644 src/lib/pages/add-network/gas-fee-details.tsx create mode 100644 src/lib/pages/add-network/index.tsx create mode 100644 src/lib/pages/add-network/network-details.tsx create mode 100644 src/lib/pages/add-network/supported-features.tsx create mode 100644 src/lib/pages/add-network/wallet-registry.tsx create mode 100644 src/pages/[network]/add-network/index.tsx create mode 100644 src/pages/add-network/index.tsx diff --git a/package.json b/package.json index 2bf22a872..2114d7780 100644 --- a/package.json +++ b/package.json @@ -69,6 +69,7 @@ "@emotion/styled": "^11.11.0", "@graphql-codegen/cli": "^5.0.0", "@graphql-typed-document-node/core": "^3.2.0", + "@hookform/resolvers": "^3.9.0", "@initia/initia.js": "0.2.5", "@initia/initia.proto": "0.2.0", "@initia/utils": "0.62.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 73af87e3b..789cb89e3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -86,6 +86,9 @@ dependencies: '@graphql-typed-document-node/core': specifier: ^3.2.0 version: 3.2.0(graphql@16.8.1) + '@hookform/resolvers': + specifier: ^3.9.0 + version: 3.9.0(react-hook-form@7.49.3) '@initia/initia.js': specifier: 0.2.5 version: 0.2.5 @@ -4940,6 +4943,14 @@ packages: graphql: 16.8.1 dev: false + /@hookform/resolvers@3.9.0(react-hook-form@7.49.3): + resolution: {integrity: sha512-bU0Gr4EepJ/EQsH/IwEzYLsT/PEj5C0ynLQ4m+GSHS+xKH4TfSelhluTgOaoc4kA5s7eCsQbM4wvZLzELmWzUg==} + peerDependencies: + react-hook-form: ^7.0.0 + dependencies: + react-hook-form: 7.49.3(react@18.2.0) + dev: false + /@humanwhocodes/config-array@0.11.14: resolution: {integrity: sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==} engines: {node: '>=10.10.0'} diff --git a/src/lib/components/WasmPageContainer.tsx b/src/lib/components/ActionPageContainer.tsx similarity index 62% rename from src/lib/components/WasmPageContainer.tsx rename to src/lib/components/ActionPageContainer.tsx index d298f434f..241af0a57 100644 --- a/src/lib/components/WasmPageContainer.tsx +++ b/src/lib/components/ActionPageContainer.tsx @@ -2,17 +2,22 @@ import type { BoxProps } from "@chakra-ui/react"; import { Flex } from "@chakra-ui/react"; import type { ReactNode } from "react"; -type WasmPageContainerProps = { +type ActionPageContainerProps = { children: ReactNode; boxProps?: BoxProps; + width?: number; }; -const WasmPageContainer = ({ children, boxProps }: WasmPageContainerProps) => ( +const ActionPageContainer = ({ + children, + boxProps, + width = 540, +}: ActionPageContainerProps) => ( ( ); -export default WasmPageContainer; +export default ActionPageContainer; diff --git a/src/lib/pages/add-network/components/AddNetworkHeader.tsx b/src/lib/pages/add-network/components/AddNetworkHeader.tsx new file mode 100644 index 000000000..5ee4a4db2 --- /dev/null +++ b/src/lib/pages/add-network/components/AddNetworkHeader.tsx @@ -0,0 +1,26 @@ +import { Alert, AlertDescription, Flex, Heading, Text } from "@chakra-ui/react"; + +interface AddNetworkHeaderProps { + title: string; +} + +export const AddNetworkHeader = ({ title }: AddNetworkHeaderProps) => ( + <> + + + Add Custom Minitia + + + {title} + + + + + + Please note that the custom Minitia you add on our website will only + be stored locally on your device. + + + + +); diff --git a/src/lib/pages/add-network/components/AddNetworkStepper.tsx b/src/lib/pages/add-network/components/AddNetworkStepper.tsx new file mode 100644 index 000000000..7bfefa541 --- /dev/null +++ b/src/lib/pages/add-network/components/AddNetworkStepper.tsx @@ -0,0 +1,91 @@ +import { Flex, Text } from "@chakra-ui/react"; + +import { CustomIcon } from "lib/components/icon"; + +const steps = [ + { label: "Network Details" }, + { label: "Supported Features" }, + { label: "Gas & Fee Details" }, + { label: "Wallet Registry" }, +]; + +interface AddNetworkStepperProps { + currentStep: number; +} + +const getStepStyles = (index: number, currentStep: number) => { + const baseStyles = { + borderRadius: "50%", + width: "24px", + height: "24px", + alignItems: "center", + justifyContent: "center", + }; + + switch (true) { + case index < currentStep: + return { + bgColor: "gray.900", + borderColor: "text.main", + textColor: "text.main", + content: ( + + + + ), + }; + case index === currentStep: + return { + bgColor: "gray.800", + borderColor: "text.main", + textColor: "text.main", + content: ( + + + {index + 1} + + + ), + }; + default: + return { + bgColor: "gray.900", + borderColor: "gray.800", + textColor: "text.dark", + content: ( + + + {index + 1} + + + ), + }; + } +}; + +export const AddNetworkStepper = ({ currentStep }: AddNetworkStepperProps) => ( + + {steps.map((step, index) => { + const { bgColor, textColor, borderColor, content } = getStepStyles( + index, + currentStep + ); + return ( + + {content} + {step.label} + + ); + })} + +); diff --git a/src/lib/pages/add-network/components/AddNetworkSubheader.tsx b/src/lib/pages/add-network/components/AddNetworkSubheader.tsx new file mode 100644 index 000000000..3c7dbf170 --- /dev/null +++ b/src/lib/pages/add-network/components/AddNetworkSubheader.tsx @@ -0,0 +1,24 @@ +import { Flex, Heading, Text } from "@chakra-ui/react"; + +interface AddNetworkSubheaderProps { + title: string; + subtitle?: string; +} + +export const AddNetworkSubheader = ({ + title, + subtitle, +}: AddNetworkSubheaderProps) => { + return ( + + + {title} + + {subtitle && ( + + {subtitle} + + )} + + ); +}; diff --git a/src/lib/pages/add-network/components/FormWrapper.tsx b/src/lib/pages/add-network/components/FormWrapper.tsx new file mode 100644 index 000000000..7efb5899e --- /dev/null +++ b/src/lib/pages/add-network/components/FormWrapper.tsx @@ -0,0 +1,53 @@ +import { Flex } from "@chakra-ui/react"; +import { useState } from "react"; + +import GasFeeDetails from "../gas-fee-details"; +import NetworkDetails from "../network-details"; +import SupportedFeatures from "../supported-features"; +import WalletRegistry from "../wallet-registry"; +import ActionPageContainer from "lib/components/ActionPageContainer"; + +import { AddNetworkStepper } from "./AddNetworkStepper"; +import { StepNavigationButtons } from "./StepNavigationButtons"; + +const StepContainer = () => { + const [currentStep, setCurrentStep] = useState(0); + + const steps = [ + , + , + , + , + ]; + + const nextStep = () => { + if (currentStep < steps.length - 1) { + setCurrentStep(currentStep + 1); + } + }; + + const prevStep = () => { + if (currentStep > 0) { + setCurrentStep(currentStep - 1); + } + }; + + return ( + + + + + + {steps[currentStep]} + + + + ); +}; + +export default StepContainer; diff --git a/src/lib/pages/add-network/components/StepNavigationButtons.tsx b/src/lib/pages/add-network/components/StepNavigationButtons.tsx new file mode 100644 index 000000000..032c6c745 --- /dev/null +++ b/src/lib/pages/add-network/components/StepNavigationButtons.tsx @@ -0,0 +1,59 @@ +import { Box, Button, Flex } from "@chakra-ui/react"; + +import { CustomIcon } from "lib/components/icon"; + +interface StepNavigationButtonsProps { + currentStep: number; + totalSteps: number; + nextStep: () => void; + prevStep: () => void; +} + +export const StepNavigationButtons = ({ + currentStep, + totalSteps, + nextStep, + prevStep, +}: StepNavigationButtonsProps) => ( + + + + + + + + {/* + The custom Minitia you add on Initiascan will only be stored locally on + your device. + */} + + +); diff --git a/src/lib/pages/add-network/gas-fee-details.tsx b/src/lib/pages/add-network/gas-fee-details.tsx new file mode 100644 index 000000000..ddba1a517 --- /dev/null +++ b/src/lib/pages/add-network/gas-fee-details.tsx @@ -0,0 +1,256 @@ +import { + Accordion, + AccordionButton, + AccordionIcon, + AccordionItem, + AccordionPanel, + Flex, + Radio, + RadioGroup, + Text, +} from "@chakra-ui/react"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useState } from "react"; +import type { Control, FieldErrors } from "react-hook-form"; +import { useForm } from "react-hook-form"; +import { z } from "zod"; + +import { ControllerInput } from "lib/components/forms"; + +import { AddNetworkHeader } from "./components/AddNetworkHeader"; +import { AddNetworkSubheader } from "./components/AddNetworkSubheader"; + +export enum GasPriceConfiguration { + STANDARD = "standard", + CUSTOM = "custom", +} + +const schema = z.object({ + gasAdjustment: z.string(), + maxGasLimit: z.string(), + feeTokenDenom: z.string(), + gasPrice: z.string().optional(), + fixedMinimumGasPrice: z.string().optional(), + lowGasPrice: z.string().optional(), + averageGasPrice: z.string().optional(), + highGasPrice: z.string().optional(), + gasForCosmosSend: z.string().optional(), + gasForIBC: z.string().optional(), +}); + +type FormData = z.infer; + +const GasOptionRender = ({ + gasConfigs, + control, + errors, +}: { + gasConfigs: GasPriceConfiguration; + control: Control; + errors: FieldErrors; +}) => ( + <> + {gasConfigs === GasPriceConfiguration.STANDARD && ( + + )} + {gasConfigs === GasPriceConfiguration.CUSTOM && ( + <> + + + + + + )} + +); + +const GasFeeDetails = () => { + const [gasConfigs, setGasConfigs] = useState( + GasPriceConfiguration.STANDARD + ); + + const { + control, + formState: { errors }, + } = useForm({ + resolver: zodResolver(schema), + mode: "all", + reValidateMode: "onChange", + defaultValues: { + gasAdjustment: "", + maxGasLimit: "", + feeTokenDenom: "", + gasPrice: "", + fixedMinimumGasPrice: "", + lowGasPrice: "", + averageGasPrice: "", + highGasPrice: "", + gasForCosmosSend: "", + gasForIBC: "", + }, + }); + + return ( + + + + + + + + + + + + + + + setGasConfigs(nextVal as GasPriceConfiguration) + } + value={gasConfigs} + > + + + + Standard Gas Price + + Set the standard gas price as the default for all gas price + configurations + + + + + + Custom Gas Prices + + Set the custom value for minimum, low, average, and high gas + price + + + + + + + + + + + Advanced Options + + + + + + + + + + + + + ); +}; + +export default GasFeeDetails; diff --git a/src/lib/pages/add-network/index.tsx b/src/lib/pages/add-network/index.tsx new file mode 100644 index 000000000..a86a49504 --- /dev/null +++ b/src/lib/pages/add-network/index.tsx @@ -0,0 +1,12 @@ +import { CelatoneSeo } from "lib/components/Seo"; + +import FormWrapper from "./components/FormWrapper"; + +export const AddNetwork = () => { + return ( + <> + + + + ); +}; diff --git a/src/lib/pages/add-network/network-details.tsx b/src/lib/pages/add-network/network-details.tsx new file mode 100644 index 000000000..845fbdc8f --- /dev/null +++ b/src/lib/pages/add-network/network-details.tsx @@ -0,0 +1,138 @@ +import { Flex, Text } from "@chakra-ui/react"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useForm } from "react-hook-form"; +import { z } from "zod"; + +import { ControllerInput } from "lib/components/forms"; + +import { AddNetworkHeader } from "./components/AddNetworkHeader"; +import { AddNetworkSubheader } from "./components/AddNetworkSubheader"; + +const URL_REGEX = + /^(https?:\/\/)?(www\.)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}(:\d+)?(\/.*)?$/; +const NetworkDetails = () => { + const { + control, + formState: { errors }, + } = useForm({ + resolver: zodResolver( + z.object({ + networkName: z.string(), + lcdUrl: z.string().regex(URL_REGEX, "Please enter a valid LCD URL"), + rpcUrl: z.string().regex(URL_REGEX, "Please enter a valid RPC URL"), + chainId: z + .string() + .regex(/^[a-z-]+$/, "Enter alphabet (a-z) and dash (-) only"), + registryChainName: z + .string() + .regex(/^[a-z]+$/, "Enter alphabet (a-z) with no spaces"), + logoUri: z.string().regex(URL_REGEX, "Please enter a valid URL"), + }) + ), + mode: "all", + reValidateMode: "onChange", + defaultValues: { + networkName: "", + lcdUrl: "", + rpcUrl: "", + chainId: "", + registryChainName: "", + logoUri: "", + }, + }); + + return ( + + + + + + + + + + + + + + + + You can edit these details later. + + + + ); +}; + +export default NetworkDetails; diff --git a/src/lib/pages/add-network/supported-features.tsx b/src/lib/pages/add-network/supported-features.tsx new file mode 100644 index 000000000..f1c228c5b --- /dev/null +++ b/src/lib/pages/add-network/supported-features.tsx @@ -0,0 +1,68 @@ +import { Checkbox, Flex, Text } from "@chakra-ui/react"; +import { useState } from "react"; + +import { AddNetworkHeader } from "./components/AddNetworkHeader"; +import { AddNetworkSubheader } from "./components/AddNetworkSubheader"; + +const initialFeatures = [ + { label: "Wasm", isChecked: false }, + { label: "Move", isChecked: false }, + { label: "NFTs", isChecked: false }, +]; + +const SupportedFeatures = () => { + const [features, setFeatures] = useState(initialFeatures); + const handleCheckboxChange = (index: number) => { + const newFeatures = [...features]; + newFeatures[index].isChecked = !newFeatures[index].isChecked; + setFeatures(newFeatures); + }; + + return ( + + + + + + {features.map((feature, index) => ( + handleCheckboxChange(index)} + _hover={{ + background: "gray.800", + }} + > + { + e.stopPropagation(); + handleCheckboxChange(index); + }} + /> + + {feature.label} + + + ))} + + + + ); +}; + +export default SupportedFeatures; diff --git a/src/lib/pages/add-network/wallet-registry.tsx b/src/lib/pages/add-network/wallet-registry.tsx new file mode 100644 index 000000000..10529c833 --- /dev/null +++ b/src/lib/pages/add-network/wallet-registry.tsx @@ -0,0 +1,381 @@ +import { Button, Flex, Heading, IconButton, Text } from "@chakra-ui/react"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useEffect, useState } from "react"; +import type { Control, FieldErrors } from "react-hook-form"; +import { useFieldArray, useForm, useWatch } from "react-hook-form"; +import { z } from "zod"; + +import { ControllerInput } from "lib/components/forms"; +import { CustomIcon } from "lib/components/icon"; + +import { AddNetworkHeader } from "./components/AddNetworkHeader"; +import { AddNetworkSubheader } from "./components/AddNetworkSubheader"; + +// interface DenomUnit { +// denom: string; +// exponent: number; +// } + +// interface Asset { +// name: string; +// base: string; +// symbol: string; +// denomUnits: DenomUnit[]; +// } + +const schema = z.object({ + bech32Prefix: z.string(), + slip44: z.string(), + assets: z.array( + z.object({ + name: z.string(), + base: z.string(), + symbol: z.string(), + denomUnits: z.array( + z.object({ + denom: z.string(), + exponent: z.string(), + }) + ), + }) + ), +}); + +type FormData = z.infer; + +interface DenomUnitsProps { + control: Control; + assetIndex: number; + errors: FieldErrors; +} +const DenomUnits = ({ control, assetIndex, errors }: DenomUnitsProps) => { + const { + fields: denomFields, + append: appendDenom, + remove: removeDenom, + } = useFieldArray({ + control, + name: `assets.${assetIndex}.denomUnits` as const, + }); + + return ( + + {denomFields.length ? ( + + {denomFields.map((unit, unitIndex) => ( + + + Denom Unit - {unitIndex + 1} + } + onClick={() => removeDenom(assetIndex)} + variant="ghost-gray" + size="sm" + /> + + + + + + + ))} + + + ) : ( + + + + )} + + ); +}; + +const WalletRegistry = () => { + const { + control, + formState: { errors }, + } = useForm({ + resolver: zodResolver(schema), + mode: "all", + reValidateMode: "onChange", + defaultValues: { + bech32Prefix: "", + slip44: "118", + assets: [], + }, + }); + + const bech32Prefix = useWatch({ + control, + name: "bech32Prefix", + }); + + const [debouncedBech32Prefix, setDebouncedBech32Prefix] = + useState(bech32Prefix); + + useEffect(() => { + const handler = setTimeout(() => { + setDebouncedBech32Prefix(bech32Prefix); + }, 500); + + return () => { + clearTimeout(handler); + }; + }, [bech32Prefix]); + + const { + fields: assetFields, + append: appendAsset, + remove: removeAsset, + } = useFieldArray({ + control, + name: "assets", + }); + + return ( + + + + + + + + + {debouncedBech32Prefix && ( + + + Account address in this Minitia will look like this: + + + {debouncedBech32Prefix} + 1cvhde2nst3qewz8x58m6tuupfk08zspeev4ud3 + + + )} + + + + + {assetFields.length ? ( + + {assetFields.map((item, index) => ( + + + + Asset + + } + onClick={() => removeAsset(index)} + variant="ghost-gray" + size="sm" + /> + + + + + + + + + Denom Units + + + + + ))} + + ) : ( + + + + )} + + {assetFields.length ? ( + + ) : ( + + Without asset information, the website remains functional. However, + with the wallet provider, assets may appear in a long format. + + )} + + + ); +}; + +export default WalletRegistry; diff --git a/src/lib/pages/admin/index.tsx b/src/lib/pages/admin/index.tsx index 9c2e534a0..415a3a586 100644 --- a/src/lib/pages/admin/index.tsx +++ b/src/lib/pages/admin/index.tsx @@ -14,6 +14,7 @@ import { useValidateAddress, useWasmConfig, } from "lib/app-provider"; +import ActionPageContainer from "lib/components/ActionPageContainer"; import { ConnectWalletAlert } from "lib/components/ConnectWalletAlert"; import { ContractInputSection } from "lib/components/ContractInputSection"; import { ContractSelectSection } from "lib/components/ContractSelectSection"; @@ -24,7 +25,6 @@ import { TextInput } from "lib/components/forms"; import { CelatoneSeo } from "lib/components/Seo"; import { TierSwitcher } from "lib/components/TierSwitcher"; import { UserDocsLink } from "lib/components/UserDocsLink"; -import WasmPageContainer from "lib/components/WasmPageContainer"; import { useTxBroadcast } from "lib/hooks"; import { useContractData } from "lib/services/wasm/contract"; import type { BechAddr, BechAddr32 } from "lib/types"; @@ -171,7 +171,7 @@ const UpdateAdmin = () => { }, [contractAddressParam, router.isReady]); return ( - + @@ -238,7 +238,7 @@ const UpdateAdmin = () => { > Update Admin - + ); }; diff --git a/src/lib/pages/deploy-script/index.tsx b/src/lib/pages/deploy-script/index.tsx index 41db5f7b6..7537658cd 100644 --- a/src/lib/pages/deploy-script/index.tsx +++ b/src/lib/pages/deploy-script/index.tsx @@ -10,12 +10,12 @@ import { useSimulateFeeQuery, } from "lib/app-provider"; import { useDeployScriptTx } from "lib/app-provider/tx/script"; +import ActionPageContainer from "lib/components/ActionPageContainer"; import { ConnectWalletAlert } from "lib/components/ConnectWalletAlert"; import { ErrorMessageRender } from "lib/components/ErrorMessageRender"; import { EstimatedFeeRender } from "lib/components/EstimatedFeeRender"; import { CelatoneSeo } from "lib/components/Seo"; import { UserDocsLink } from "lib/components/UserDocsLink"; -import WasmPageContainer from "lib/components/WasmPageContainer"; import { useTxBroadcast } from "lib/hooks"; import type { AbiFormData, ExposedFunction, Option } from "lib/types"; import { composeScriptMsg, getAbiInitialData } from "lib/utils"; @@ -137,7 +137,7 @@ export const DeployScript = () => { return ( <> - + Script @@ -214,7 +214,7 @@ export const DeployScript = () => { alignSelf="flex-start" /> )} - +