diff --git a/components/admin/blueprints-admin.tsx b/components/admin/blueprints-admin.tsx new file mode 100644 index 0000000..b6d3a70 --- /dev/null +++ b/components/admin/blueprints-admin.tsx @@ -0,0 +1,14 @@ +import { Card, Flex, VStack } from "@chakra-ui/react"; +import { CreateOrUpdateBlueprintForm } from "@/components/forms/create-or-update-blueprint-form"; + +export const BlueprintsAdmin = () => { + return ( + + + + + + + + ); +}; diff --git a/components/admin/create-blueprint-modal.tsx b/components/admin/create-blueprint-modal.tsx new file mode 100644 index 0000000..e14839f --- /dev/null +++ b/components/admin/create-blueprint-modal.tsx @@ -0,0 +1,17 @@ +import { ModalProps } from "@chakra-ui/modal"; +import { GenericModal } from "@/components/GenericModal"; +import { CreateOrUpdateBlueprintForm } from "@/components/forms/create-or-update-blueprint-form"; + +export const CreateBlueprintModal = ({ + registryId, + ...modalProps +}: { registryId?: string } & Omit) => { + return ( + + + + ); +}; diff --git a/components/admin/create-registry-modal.tsx b/components/admin/create-registry-modal.tsx index 9fdbbbf..dd7f6e7 100644 --- a/components/admin/create-registry-modal.tsx +++ b/components/admin/create-registry-modal.tsx @@ -10,6 +10,8 @@ import { CreateUpdateRegistryFormValues, } from "@/components/forms/create-or-update-registry-form"; import { useChainId } from "wagmi"; +import { useCreateClaims } from "@/hooks/useCreateClaims"; +import { useHypercertClient } from "@/components/providers"; export const CreateRegistryModal = ({ initialValues, @@ -21,13 +23,26 @@ export const CreateRegistryModal = ({ const address = useAddress(); const toast = useToast(); const chainId = useChainId(); + const client = useHypercertClient(); const { refetch } = useMyRegistries(); + const { mutateAsync: createClaims } = useCreateClaims(); const onConfirm = async ({ claims, ...registry }: CreateUpdateRegistryFormValues) => { + if (!client) { + toast({ + title: "Error", + description: "Client not initialized", + status: "error", + duration: 9000, + isClosable: true, + }); + return; + } + if (!address) { toast({ title: "Error", @@ -83,22 +98,30 @@ export const CreateRegistryModal = ({ status: "success", }); - const claimInserts: ClaimInsert[] = claims.map(({ hypercert_id }) => ({ - registry_id: insertedRegistry.id, - hypercert_id, - chain_id: chainId, - admin_id: address, - })); - - const { error: insertClaimsError } = await supabase - .from("claims") - .insert(claimInserts) - .select(); - - if (insertClaimsError) { + try { + const claimInserts: ClaimInsert[] = await Promise.all( + claims.map(async ({ hypercert_id }) => { + const claim = await client.indexer.claimById(hypercert_id); + if (!claim.claim) { + throw new Error("Claim not found"); + } + return { + registry_id: insertedRegistry.id, + hypercert_id, + chain_id: chainId, + admin_id: address, + owner_id: claim.claim.owner, + }; + }), + ); + await createClaims({ + claims: claimInserts, + }); + } catch (insertClaimsError) { + console.error(insertClaimsError); toast({ title: "Error", - description: insertClaimsError.message, + description: "Something went wrong with creating claims", status: "error", duration: 9000, isClosable: true, diff --git a/components/admin/delete-blueprint-button.tsx b/components/admin/delete-blueprint-button.tsx new file mode 100644 index 0000000..5b37ba2 --- /dev/null +++ b/components/admin/delete-blueprint-button.tsx @@ -0,0 +1,59 @@ +import { IconButton, useDisclosure, useToast } from "@chakra-ui/react"; +import { useMyRegistries } from "@/hooks/useMyRegistries"; +import { AiFillDelete } from "react-icons/ai"; +import { AlertDialog } from "@/components/dialogs/alert-confirmation-dialog"; +import { useDeleteBlueprint } from "@/hooks/useDeleteBlueprint"; + +export const DeleteBlueprintButton = ({ + blueprintId, + size = "sm", +}: { + blueprintId: number; + size?: string; +}) => { + const { refetch } = useMyRegistries(); + const { onClose, onOpen, isOpen } = useDisclosure(); + + const toast = useToast(); + const { mutateAsync: deleteBlueprintAsync } = useDeleteBlueprint(); + + const onDeleteBlueprint = async () => { + try { + await deleteBlueprintAsync(blueprintId); + } catch (e) { + console.error(e); + toast({ + title: "Error", + description: "Could not delete blueprint", + status: "error", + duration: 9000, + isClosable: true, + }); + } + + await refetch(); + toast({ + title: "Success", + description: "Blueprint deleted", + status: "success", + }); + }; + + return ( + <> + } + colorScheme="red" + onClick={onOpen} + /> + onDeleteBlueprint()} + onClose={onClose} + isOpen={isOpen} + /> + + ); +}; diff --git a/components/admin/my-blueprints-admin.tsx b/components/admin/my-blueprints-admin.tsx new file mode 100644 index 0000000..a487d94 --- /dev/null +++ b/components/admin/my-blueprints-admin.tsx @@ -0,0 +1,87 @@ +import { + Card, + Flex, + Spinner, + Table, + TableContainer, + Tbody, + Td, + Th, + Thead, + Tr, + VStack, +} from "@chakra-ui/react"; +import { useMyBlueprints } from "@/hooks/useMyBlueprints"; +import { formatAddress } from "@/utils/formatting"; +import Link from "next/link"; +import { useRouter } from "next/router"; +import { BlueprintMinter } from "@/components/minting/blueprint-minter"; + +export const MyBlueprintsAdmin = () => { + const { data, isLoading } = useMyBlueprints(); + const { query, push } = useRouter(); + + if (isLoading) { + return ; + } + + if (!data) { + return null; + } + + const blueprintId = query["blueprintId"]; + const parsedBluePrintId = parseInt(blueprintId as string); + + return ( + + + + {blueprintId ? ( + push("/admin/my-claims/")} + /> + ) : ( + + + + + + + + + + + + {data.data?.map((blueprint) => ( + + {/* + // @ts-ignore */} + + + + + + + ))} + +
NameRegistryCreated onCreated by
{blueprint.form_values?.name}{blueprint.registries?.name}{new Date(blueprint.created_at).toLocaleString()}{formatAddress(blueprint.admin_id)} + + Mint + {" "} +
+
+ )} +
+
+
+ ); +}; diff --git a/components/admin/my-claims-admin.tsx b/components/admin/my-claims-admin.tsx new file mode 100644 index 0000000..de9356f --- /dev/null +++ b/components/admin/my-claims-admin.tsx @@ -0,0 +1,59 @@ +import { + Card, + Flex, + Heading, + TableContainer, + Tr, + Table, + VStack, + Thead, + Tbody, + Th, + Center, + Spinner, +} from "@chakra-ui/react"; +import { useMyClaims } from "@/hooks/useMyClaims"; +import { ClaimRow } from "@/components/admin/registries-admin"; + +export const MyClaimsAdmin = () => { + const { data, isLoading } = useMyClaims(); + + if (isLoading) { + return ; + } + + const myClaims = data?.data; + + return ( + + + + {myClaims?.length ? ( + + + + + + + + + + + + + {myClaims.map((claim) => ( + + ))} + +
NameChainAdminExternal urlDescription
+
+ ) : ( +
+ No claims found +
+ )} +
+
+
+ ); +}; diff --git a/components/admin/registries-admin.tsx b/components/admin/registries-admin.tsx index 746ac72..1a4b360 100644 --- a/components/admin/registries-admin.tsx +++ b/components/admin/registries-admin.tsx @@ -15,6 +15,9 @@ import { Tbody, Th, Link, + TableCaption, + Center, + Spinner, } from "@chakra-ui/react"; import { useMyRegistries } from "@/hooks/useMyRegistries"; import { CreateRegistryModal } from "@/components/admin/create-registry-modal"; @@ -26,6 +29,8 @@ import { ClaimEntity } from "@/types/database-entities"; import { useHypercertById } from "@/hooks/useHypercertById"; import { formatAddress } from "@/utils/formatting"; import { DeleteClaimButton } from "@/components/admin/delete-claim-button"; +import { DeleteBlueprintButton } from "@/components/admin/delete-blueprint-button"; +import { CreateBlueprintModal } from "@/components/admin/create-blueprint-modal"; export const RegistriesAdmin = () => { const { @@ -34,6 +39,12 @@ export const RegistriesAdmin = () => { onOpen: createOnOpen, } = useDisclosure(); + const { + isOpen: createBlueprintIsOpen, + onClose: createBlueprintOnClose, + onOpen: createBlueprintOnOpen, + } = useDisclosure(); + const { data } = useMyRegistries(); const [selectedRegistry, setSelectedRegistry] = @@ -78,8 +89,9 @@ export const RegistriesAdmin = () => { - + + Claims @@ -96,6 +108,49 @@ export const RegistriesAdmin = () => {
Name
+ + + Blueprints + + + + + + + + + {registry.blueprints.map((blueprint) => ( + + + + + + + ))} + +
NameMinter addressCreated on
+ {/* + // @ts-ignore */} + {blueprint.form_values.name || "No name"} + {formatAddress(blueprint.minter_address)} + {new Date(blueprint.created_at).toLocaleDateString()} + + +
+
+
+ +
))} @@ -105,15 +160,37 @@ export const RegistriesAdmin = () => { onClose={onModalClose} initialValues={selectedRegistry} /> + { + createBlueprintOnClose(); + setSelectedRegistry(undefined); + }} + registryId={selectedRegistry?.id} + /> ); }; -const ClaimRow = ({ hypercert_id, chain_id, id }: {} & ClaimEntity) => { - const { data } = useHypercertById(hypercert_id); +export const ClaimRow = ({ hypercert_id, chain_id, id }: {} & ClaimEntity) => { + const { data, isLoading } = useHypercertById(hypercert_id); + + if (isLoading) { + return ( + + + + + + ); + } if (!data) { - return
Hypercert not found
; + return ( + + Hypercert not found + + ); } return ( diff --git a/components/admin/registry-selector.tsx b/components/admin/registry-selector.tsx index a6bb723..014db4e 100644 --- a/components/admin/registry-selector.tsx +++ b/components/admin/registry-selector.tsx @@ -1,5 +1,8 @@ import AsyncSelect from "react-select/async"; import { supabase } from "@/lib/supabase"; +import { useAddress } from "@/hooks/useAddress"; +import { Props } from "react-select"; +import React, { forwardRef } from "react"; const getRegistryOptions = async (name: string) => { return supabase @@ -11,6 +14,39 @@ const getRegistryOptions = async (name: string) => { }); }; +const getMyRegistryOptions = async (address: string, name: string) => { + return supabase + .from("registries") + .select("id, name") + .eq("admin_id", address) + .ilike("name", `%${name}%`) + .then(({ data }) => { + return ( + data?.map(({ id, name }) => ({ + value: id as string, + label: name as string, + })) || [] + ); + }); +}; + +export const SingleRegistrySelector = forwardRef( + function SingleRegistrySelectorRef( + props: Omit, + ref: React.Ref, + ) { + const address = useAddress(); + return ( + getMyRegistryOptions(address || "", name)} + defaultOptions + /> + ); + }, +); + export const RegistrySelector = ({ onChange, }: { diff --git a/components/forms/create-or-update-blueprint-form.tsx b/components/forms/create-or-update-blueprint-form.tsx new file mode 100644 index 0000000..ca5bf45 --- /dev/null +++ b/components/forms/create-or-update-blueprint-form.tsx @@ -0,0 +1,134 @@ +import { Controller, useForm } from "react-hook-form"; +import { + MintingForm, + MintingFormValues, +} from "@/components/minting/minting-form"; +import { + FormControl, + FormErrorMessage, + FormLabel, + Input, + useToast, + VStack, +} from "@chakra-ui/react"; +import { useAddress } from "@/hooks/useAddress"; +import { useHypercertClient } from "@/components/providers"; +import { useCreateBlueprint } from "@/hooks/useCreateBlueprint"; +import { SingleRegistrySelector } from "@/components/admin/registry-selector"; +import { useFetchRegistryById } from "@/hooks/useFetchRegistryById"; +import { useEffect } from "react"; + +interface FormValues { + address: string; + registryId: { label: string; value: string }; +} + +export const CreateOrUpdateBlueprintForm = ({ + registryId, + onComplete, +}: { + registryId?: string; + onComplete?: () => void; +}) => { + const address = useAddress(); + const { data: registryData } = useFetchRegistryById(registryId); + + const { + control, + register, + getValues, + setValue, + formState: { errors }, + } = useForm({ + reValidateMode: "onBlur", + defaultValues: { + address, + }, + }); + + useEffect(() => { + if (registryData?.data) { + setValue("registryId", { + value: registryData.data.id, + label: registryData.data.name, + }); + } + }, [registryData?.data, setValue]); + + const toast = useToast(); + const client = useHypercertClient(); + const { mutateAsync } = useCreateBlueprint(); + + const onSubmitBluePrint = async (values: MintingFormValues) => { + const address = getValues("address"); + const registryId = getValues("registryId"); + + if (!client) { + toast({ + title: "Client is not initialized", + status: "error", + }); + return; + } + + if (!address) { + toast({ + title: "Blueprint address is required", + status: "error", + }); + return; + } + + if (!registryId) { + toast({ + title: "Registry is required", + status: "error", + }); + return; + } + + try { + await mutateAsync({ + ...values, + address, + registryId: registryId.value, + }); + toast({ + title: "Blueprint created", + status: "success", + }); + } catch (e) { + console.error(e); + toast({ + title: "Error creating blueprint", + status: "error", + }); + } + + onComplete?.(); + }; + + return ( + + + Minter Address + + {errors.address?.message} + + + Registry ID + } + name={"registryId"} + /> + {errors.registryId?.message} + + + + ); +}; diff --git a/components/interaction-modal.tsx b/components/interaction-modal.tsx new file mode 100644 index 0000000..ab7a815 --- /dev/null +++ b/components/interaction-modal.tsx @@ -0,0 +1,124 @@ +import React, { + createContext, + PropsWithChildren, + useContext, + useEffect, + useRef, + useState, +} from "react"; +import { + Box, + Modal, + Spinner, + Step, + StepDescription, + StepIcon, + StepIndicator, + StepNumber, + Stepper, + StepSeparator, + StepStatus, + StepTitle, + useDisclosure, + useSteps, +} from "@chakra-ui/react"; +import { ModalBody, ModalContent, ModalOverlay } from "@chakra-ui/modal"; + +type Step = { + title: string; + description: string; +}; + +interface Props { + onOpen: (steps: Step[]) => void; + onClose: () => void; + setStep: (step: string) => void; +} + +const defaultValues = { + setStep: () => {}, + onOpen: () => {}, + onClose: () => {}, +}; + +const InteractionModalContext = createContext(defaultValues); + +export const useInteractionModal = () => useContext(InteractionModalContext); + +export const InteractionDialogProvider = ({ children }: PropsWithChildren) => { + const { isOpen, onOpen, onClose } = useDisclosure(); + const [steps, setSteps] = useState([]); + + const stepsRef = useRef(steps); + + useEffect(() => { + stepsRef.current = steps; + }, [steps]); + + const { activeStep, setActiveStep } = useSteps({ + index: 0, + count: steps.length, + }); + + const onSetStep = (step: string) => { + const index = stepsRef.current.findIndex((s) => s.title === step); + setActiveStep(index); + }; + + const onOpenAndSetSteps = (steps: Step[]) => { + setSteps(steps); + stepsRef.current = steps; + setActiveStep(0); + onOpen(); + }; + + const onCloseAndResetSteps = () => { + setSteps([]); + stepsRef.current = []; + setActiveStep(0); + onClose(); + }; + + return ( + + {children} + + + + + + {steps.map((step, index) => ( + + + } + incomplete={} + active={} + /> + + + + {step.title} + {step.description} + + + + + ))} + + + + + + ); +}; diff --git a/components/minting/blueprint-minter.tsx b/components/minting/blueprint-minter.tsx new file mode 100644 index 0000000..8f17ed5 --- /dev/null +++ b/components/minting/blueprint-minter.tsx @@ -0,0 +1,325 @@ +import { useBlueprintById } from "@/hooks/useBlueprintById"; +import { Heading, HStack, Spinner, useToast } from "@chakra-ui/react"; +import { + MintingForm, + MintingFormValues, +} from "@/components/minting/minting-form"; +import { HypercertPreview } from "@/components/minting/hypercert-preview"; +import { useRef } from "react"; +import { exportAsImage } from "@/lib/exportToImage"; +import { useHypercertClient } from "@/components/providers"; +import { + HypercertMetadata, + TransferRestrictions, + validateMetaData, + validateClaimData, +} from "@hypercerts-org/sdk"; +import { ContractReceipt } from "@ethersproject/contracts"; +import { BigNumber } from "@ethersproject/bignumber"; +import { useCreateClaims } from "@/hooks/useCreateClaims"; +import { useDeleteBlueprint } from "@/hooks/useDeleteBlueprint"; +import { useInteractionModal } from "@/components/interaction-modal"; +import { useAddress } from "@/hooks/useAddress"; + +const formValuesToHypercertMetadata = ( + values: MintingFormValues, + image: string, +): HypercertMetadata => { + const claimData = { + work_scope: { + value: values.workScope.split(",").map((x) => x.trim()), + }, + contributors: { + value: values.contributors.split(",").map((x) => x.trim()), + }, + impact_scope: { + value: [], + }, + rights: { + value: [], + }, + impact_timeframe: { + value: [], + }, + work_timeframe: { + value: [ + Math.floor(values.workStart.getTime() / 1000), + Math.floor(values.workEnd.getTime() / 1000), + ], + }, + }; + + const { errors: claimDataErrors, valid: claimDataValid } = + validateClaimData(claimData); + + if (!claimDataValid) { + console.error(claimDataErrors); + throw new Error("Claim data is not valid"); + } + + const metaData = { + name: values.name, + description: values.description, + external_url: values.externalUrl, + image: image, + hypercert: claimData, + }; + + const { errors: metaDataErrors, valid: metaDataValid } = + validateMetaData(metaData); + + if (!metaDataValid) { + console.error(metaDataErrors); + throw new Error("Metadata is not valid"); + } + + return metaData; +}; + +const constructClaimIdFromContractReceipt = (receipt: ContractReceipt) => { + const { events } = receipt; + + if (!events) { + throw new Error("No events in receipt"); + } + + const claimEvent = events.find((e) => e.event === "TransferSingle"); + + if (!claimEvent) { + throw new Error("TransferSingle event not found"); + } + + const { args } = claimEvent; + + if (!args) { + throw new Error("No args in event"); + } + + const tokenIdBigNumber = args[3] as BigNumber; + + if (!tokenIdBigNumber) { + throw new Error("No tokenId arg in event"); + } + + const contractId = receipt.to.toLowerCase(); + const tokenId = tokenIdBigNumber.toString(); + + return `${contractId}-${tokenId}`; +}; + +export const BlueprintMinter = ({ + blueprintId, + onComplete, +}: { + blueprintId: number; + onComplete?: () => void; +}) => { + const { data: blueprint, isLoading } = useBlueprintById(blueprintId); + const ref = useRef(null); + const toast = useToast(); + const client = useHypercertClient(); + const { mutateAsync: createClaims } = useCreateClaims(); + const { mutateAsync: deleteBlueprint } = useDeleteBlueprint(); + const { onOpen, setStep, onClose } = useInteractionModal(); + const address = useAddress(); + + const onMint = async (values: MintingFormValues) => { + if (!address) { + toast({ + title: "Error", + description: "Address not found", + status: "error", + duration: 9000, + isClosable: true, + }); + return; + } + + if (!blueprint?.data?.registries) { + toast({ + title: "Error", + description: "Blueprint not found", + status: "error", + duration: 9000, + isClosable: true, + }); + return; + } + + if (!client) { + toast({ + title: "Error", + description: "Client not initialized", + status: "error", + duration: 9000, + isClosable: true, + }); + return; + } + + const steps = [ + { + title: "Generate image", + description: "Generating image", + }, + { + title: "Minting", + description: "Minting", + }, + { + title: "Adding to registry", + description: "Adding to registry", + }, + { + title: "Deleting blueprint", + description: "Deleting blueprint", + }, + ]; + + onOpen(steps); + setStep("Generate image"); + const image = await exportAsImage(ref); + + if (!image) { + toast({ + title: "Error", + description: "Could not export image", + status: "error", + duration: 9000, + isClosable: true, + }); + onClose(); + return; + } + + let contractReceipt: ContractReceipt | undefined; + + setStep("Minting"); + try { + const claimData = formValuesToHypercertMetadata(values, image); + const mintResult = await client.mintClaim( + claimData, + 1000, + TransferRestrictions.FromCreatorOnly, + ); + contractReceipt = await mintResult.wait(); + } catch (e) { + console.error(e); + toast({ + title: "Error", + description: "Could not mint hypercert", + status: "error", + duration: 9000, + isClosable: true, + }); + onClose(); + return; + } + + let claimId: string | undefined; + + try { + claimId = constructClaimIdFromContractReceipt(contractReceipt); + } catch (e) { + console.error(e); + toast({ + title: "Error", + description: "Could not construct claimId", + status: "error", + duration: 9000, + isClosable: true, + }); + onClose(); + return; + } + + setStep("Adding to registry"); + try { + await createClaims({ + claims: [ + { + hypercert_id: claimId, + registry_id: blueprint.data.registry_id, + admin_id: blueprint.data.admin_id, + chain_id: blueprint.data.registries.chain_id, + owner_id: address, + }, + ], + }); + toast({ + title: "Success", + description: "Claim added to registry", + status: "success", + duration: 9000, + isClosable: true, + }); + } catch (e) { + console.log(e); + toast({ + title: "Error", + description: "Could not add claim to registry", + status: "error", + duration: 9000, + isClosable: true, + }); + onClose(); + return; + } + + setStep("Deleting blueprint"); + try { + await deleteBlueprint(blueprintId); + toast({ + title: "Success", + description: "Blueprint deleted", + status: "success", + duration: 9000, + isClosable: true, + }); + onClose(); + } catch (e) { + console.log(e); + toast({ + title: "Error", + description: "Could not delete blueprint", + status: "error", + duration: 9000, + isClosable: true, + }); + onClose(); + return; + } + + onComplete?.(); + }; + + if (isLoading) { + return ; + } + + if (!blueprint?.data) { + return Blueprint not found; + } + const initialValues = blueprint.data + ?.form_values! as unknown as MintingFormValues; + + const workEnd = new Date(initialValues.workEnd); + const workStart = new Date(initialValues?.workStart); + + const values = { + ...initialValues, + workEnd, + workStart, + }; + + return ( + + + + + ); +}; diff --git a/components/minting/hypercert-preview.tsx b/components/minting/hypercert-preview.tsx new file mode 100644 index 0000000..e324cd9 --- /dev/null +++ b/components/minting/hypercert-preview.tsx @@ -0,0 +1,131 @@ +import { MintingFormValues } from "@/components/minting/minting-form"; +import { MutableRefObject } from "react"; + +export const HypercertPreview = ({ + values, + imageRef, +}: { + imageRef: MutableRefObject; + values: Partial; +}) => { + return ( +
+

+ {values?.name ?? "Name of the chapter"} +

+ +
+ +
+
+ {values.workScope + ?.split(", ") + .map((w) => w.trim()) + .map((w) => ( + + {w.toLowerCase()} + + ))} +
+ +
+ Timeframe + + {values.workStart?.toLocaleDateString() ?? "Start Date"} + {" — "} + {values.workEnd?.toLocaleDateString() ?? "End Date"} + +
+
+ +
+ +
+
+ ); +}; diff --git a/components/minting/minting-form.tsx b/components/minting/minting-form.tsx new file mode 100644 index 0000000..c1210c1 --- /dev/null +++ b/components/minting/minting-form.tsx @@ -0,0 +1,109 @@ +import { useForm } from "react-hook-form"; +import { + Button, + Flex, + FormControl, + FormErrorMessage, + FormLabel, + Input, + Textarea, + VStack, +} from "@chakra-ui/react"; +import { SingleDatepicker } from "chakra-dayzed-datepicker"; + +export interface MintingFormValues { + name: string; + workScope: string; + description: string; + externalUrl: string; + workStart: Date; + workEnd: Date; + contributors: string; +} + +// Default values minting form for testing +export const defaultMintingFormValues: MintingFormValues = { + name: "Test", + workScope: "Test", + description: "Test", + externalUrl: "Test", + workStart: new Date(), + workEnd: new Date(), + contributors: "Test", +}; + +const useMintingForm = (initialValues?: MintingFormValues) => + useForm({ + defaultValues: initialValues || defaultMintingFormValues, + }); + +export const MintingForm = ({ + onSubmit, + initialValues, + buttonLabel = "Submit", +}: { + onSubmit: (values: MintingFormValues) => void; + initialValues?: MintingFormValues; + buttonLabel?: string; +}) => { + const { + register, + setValue, + watch, + formState: { errors }, + handleSubmit, + } = useMintingForm(initialValues); + + return ( +
+ + + + Name + + {errors.name?.message} + + + Work Scope + + {errors.workScope?.message} + + + Description +