diff --git a/frontend/components/blueprint-create.tsx b/frontend/components/blueprint-create.tsx deleted file mode 100644 index 876b6a6c..00000000 --- a/frontend/components/blueprint-create.tsx +++ /dev/null @@ -1,487 +0,0 @@ -import { useAccountLowerCase } from "../hooks/account"; -import { useMintBlueprintToRegistry } from "../hooks/createBlueprintInRegistry"; -import { useListRegistries } from "../hooks/list-registries"; -import { parseListFromString } from "../lib/parsing"; -import { useConfetti } from "./confetti"; -import { useContractModal } from "./contract-interaction-dialog-context"; -import { DATE_INDEFINITE, DateIndefinite, FormContext } from "./forms"; -import { formatHypercertData } from "@hypercerts-org/sdk"; -import { DataProvider } from "@plasmicapp/loader-nextjs"; -import dayjs from "dayjs"; -import { Formik, FormikProps } from "formik"; -import html2canvas from "html2canvas"; -import _ from "lodash"; -import { useRouter } from "next/router"; -import qs from "qs"; -import React, { ReactNode } from "react"; -import { toast } from "react-toastify"; -import * as Yup from "yup"; - -/** - * Constants - */ -const FORM_SELECTOR = "currentForm"; -const IMAGE_SELECTOR = "hypercertimage"; -export const NAME_MIN_LENGTH = 2; -export const NAME_MAX_LENGTH = 50; - -export const DESCRIPTION_MIN_LENGTH = 20; -export const DESCRIPTION_MAX_LENGTH = 1500; - -export const DEFAULT_NUM_FRACTIONS = 10000; -export const DEFAULT_HYPERCERT_VERSION = "0.0.1"; - -//const DEFAULT_TIME = dayjs().format("YYYY-MM-DD"); -const DEFAULT_TIME = dayjs(); -const DEFAULT_FORM_DATA: BlueprintCreateFormData = { - registryId: "", - minterAddress: "", - name: "", - description: "", - externalLink: "", - logoUrl: "", - //logoImage: null, - bannerUrl: "", - //bannerImage: null, - impactScopes: ["all"] as string[], - //impactTimeStart: DEFAULT_TIME.format("YYYY-MM-DD"), - impactTimeEnd: DEFAULT_TIME.format("YYYY-MM-DD"), - workScopes: "", - workTimeStart: DEFAULT_TIME.format("YYYY-MM-DD"), - workTimeEnd: DEFAULT_TIME.format("YYYY-MM-DD"), - rights: ["Public Display"] as string[], - contributors: "", - agreeContributorsConsent: false, - agreeTermsConditions: false, - // Hidden - backgroundColor: "", - backgroundVectorArt: "", - metadataProperties: "", -}; - -interface BlueprintCreateFormData { - registryId: string; - minterAddress: string; - name: string; - description: string; - externalLink: string; - logoUrl: string; - //logoImage: File | null; - bannerUrl: string; - //bannerImage: File | null; - impactScopes: string[]; - //impactTimeStart?: string; - impactTimeEnd?: string | DateIndefinite; - workScopes: string; - workTimeStart?: string; - workTimeEnd?: string; - rights: string[]; - contributors: string; - agreeContributorsConsent: boolean; - agreeTermsConditions: boolean; - // Hidden - backgroundColor: string; - backgroundVectorArt: string; - metadataProperties: string; -} - -/** - * Generic utility function to check for valid URLs - * - We should probably move this to common.ts or util.ts - * @param value - * @param opts - * @returns - */ -const isValidUrl = ( - value: any, - opts: { - emptyAllowed?: boolean; - ipfsAllowed?: boolean; - }, -) => { - // Check empty, null, or undefined - if (opts.emptyAllowed && !value) { - return true; - } else if (!value) { - return false; - } - - // Check IPFS - const isIpfsUrl = value.match(/^(ipfs):\/\//); - if (opts.ipfsAllowed && isIpfsUrl) { - return true; - } - - try { - const urlSchema = Yup.string().url(); - urlSchema.validateSync(value); - return true; - } catch (e) { - return false; - } -}; - -/** - * Converts raw form data to a query string - * @param values - * @returns - */ -const formDataToQueryString = (values: Record) => { - // We will serialize our Dayjs objects - const formatDate = (key: string) => { - if (values[key] === DATE_INDEFINITE) { - values[key] = DATE_INDEFINITE; - } else if (values[key] && values[key].format) { - values[key] = values[key].format("YYYY-MM-DD"); - } - }; - ["impactTimeStart", "impactTimeEnd", "workTimeStart", "workTimeEnd"].forEach( - formatDate, - ); - const filteredValues = _.chain(values).pickBy().value(); - return qs.stringify(filteredValues); -}; - -/** - * Converts a query string into raw form data - * @param query - * @returns - */ -const queryStringToFormData = (query?: string) => { - const rawValues = qs.parse(query ?? ""); - const parseValue = (v: any) => { - return v === DATE_INDEFINITE ? DATE_INDEFINITE : dayjs(v as string); - }; - const values = { - ...rawValues, - // we need to parse dates to match the expected types - //impactTimeStart: parseValue(rawValues["impactTimeStart"]), - impactTimeEnd: parseValue(rawValues["impactTimeEnd"]), - workTimeStart: parseValue(rawValues["workTimeStart"]), - workTimeEnd: parseValue(rawValues["workTimeEnd"]), - }; - return values as any; -}; - -/** - * Form validation rules - */ -const ValidationSchema = Yup.object().shape({ - registryId: Yup.string().required("Required"), - minterAddress: Yup.string().required("Required"), - name: Yup.string() - .min(NAME_MIN_LENGTH, `Name must be at least ${NAME_MIN_LENGTH} characters`) - .max(NAME_MAX_LENGTH, `Name must be at most ${NAME_MAX_LENGTH} characters`) - .required("Required"), - description: Yup.string() - .min( - DESCRIPTION_MIN_LENGTH, - `Description must be at least ${DESCRIPTION_MIN_LENGTH} characters`, - ) - .max( - DESCRIPTION_MAX_LENGTH, - `Description must be at most ${DESCRIPTION_MAX_LENGTH} characters`, - ) - .required("Required"), - externalLink: Yup.string().test( - "valid uri", - "Please enter a valid URL", - (value) => - isValidUrl(value, { - emptyAllowed: true, - ipfsAllowed: true, - }), - ), - logoUrl: Yup.string().test("valid uri", "Please enter a valid URL", (value) => - isValidUrl(value, { - emptyAllowed: true, - ipfsAllowed: false, - }), - ), - bannerUrl: Yup.string().test( - "valid uri", - "Please enter a valid URL", - (value) => - isValidUrl(value, { - emptyAllowed: true, - ipfsAllowed: false, - }), - ), - impactScopes: Yup.array().min(1, "Please choose at least 1 item"), - impactTimeEnd: Yup.lazy((val: any) => { - switch (typeof val) { - case "string": - return Yup.string(); - default: - return Yup.date().when("workTimeStart", (workTimeStart) => { - return Yup.date().min( - workTimeStart, - "End date must be after start date", - ); - }); - } - }), - workScopes: Yup.string() - .required("Required") - .min( - NAME_MIN_LENGTH, - `Work scopes must be at least ${NAME_MIN_LENGTH} characters`, - ) - .test("no duplicates", "Please remove duplicate items", (value) => { - if (!value) { - return true; - } - const items = parseListFromString(value, { lowercase: "all" }); - const dedup = parseListFromString(value, { - lowercase: "all", - deduplicate: true, - }); - return _.isEqual(items, dedup); - }), - workTimeEnd: Yup.date().when("workTimeStart", (workTimeStart) => { - return Yup.date().min(workTimeStart, "End date must be after start date"); - }), - rights: Yup.array().min(1), - contributors: Yup.string() - .required("Required") - .test("no duplicates", "Please remove duplicate items", (value) => { - if (!value) { - return true; - } - const items = parseListFromString(value, { lowercase: "all" }); - const dedup = parseListFromString(value, { - lowercase: "all", - deduplicate: true, - }); - return _.isEqual(items, dedup); - }), - agreeContributorsConsent: Yup.boolean().oneOf( - [true], - "You must get the consent of contributors before creating", - ), - agreeTermsConditions: Yup.boolean().oneOf( - [true], - "You must agree to the terms and conditions", - ), -}); - -/** - * Hypercert creation form logic using Formik - * - For the actual layout of form elements, - * we assume it's passed in via the `children` prop. - * - Use the form elements defined in `./forms.tsx` - * - Make sure that there is a form element with a `fieldName` - * for each field in HypercertCreateFormData - */ -export interface HypercertCreateFormProps { - className?: string; // Plasmic CSS class - children?: ReactNode; // Form elements -} - -export function BlueprintCreateForm(props: HypercertCreateFormProps) { - const { data: registries = [] } = useListRegistries(); - const registryOptions = registries.map((r: any) => `${r.name} - ${r.id}`); - const { className, children } = props; - const { address } = useAccountLowerCase(); - const { push } = useRouter(); - const { hideModal } = useContractModal(); - const confetti = useConfetti(); - - // Query string - const [initialQuery, setInitialQuery] = React.useState( - undefined, - ); - // Load the querystring into React state only once on initial page load - React.useEffect(() => { - if (!initialQuery) { - window.location.hash.startsWith("#") - ? setInitialQuery(window.location.hash.slice(1)) - : setInitialQuery(window.location.hash); - } - }, [initialQuery]); - - const onComplete = async () => { - hideModal(); - confetti && - (await confetti.addConfetti({ - emojis: ["🌈", "⚡️", "💥", "✨", "💫", "🌸"], - })); - push("/app/dashboard"); - }; - - const { mutate: createBlueprint, isLoading: createBlueprintPending } = - useMintBlueprintToRegistry({ - onComplete, - }); - - return ( -
- { - // console.log(values); - if (typeof initialQuery !== "undefined") { - // The useEffect has run already, so it's safe to just update the query string directly - //const querystring = formDataToQueryString(values); - //const path = `${window.location.pathname}#${querystring}`; - //window.history.pushState(null, "", path); - } - }} - initialValues={{ - ...DEFAULT_FORM_DATA, - ...queryStringToFormData(initialQuery), - }} - enableReinitialize - onSubmit={async (values, { setSubmitting }) => { - const image = await exportAsImage(IMAGE_SELECTOR); - const metaData = formatValuesToMetaData( - values, - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - address!, - image, - ); - console.log(`Metadata(valid=${metaData.valid}): `, metaData.data); - if (metaData.data) { - const registryId = values.registryId.split(" - ")[1]; - //return; // Used for testing - - if (!registryId) { - toast("Error creating blueprint. Please contact the team.", { - type: "error", - }); - console.error("Registry ID not found"); - return; - } - createBlueprint({ - registryId, - value: formDataToQueryString(values), - minterAddress: values.minterAddress, - }); - } else { - toast("Error creating hypercert. Please contact the team.", { - type: "error", - }); - console.error("SDK formatting errors: ", metaData.errors); - } - - if (!createBlueprintPending) { - setSubmitting(false); - } - }} - > - {(formikProps: FormikProps) => ( - - -
{ - e.preventDefault(); - console.log("Submitting form..."); - console.log("Form values: ", formikProps.values); - console.log("Form errors: ", formikProps.errors); - formikProps.handleSubmit(); - }} - > - {children} -
-
-
- )} -
-
- ); -} - -const formatValuesToMetaData = ( - val: BlueprintCreateFormData, - address: string, - image?: string, -) => { - // Split contributor names and addresses. - // - make sure addresses are always lower case - const contributorNamesAndAddresses = parseListFromString(val.contributors, { - lowercase: "addresses", - }); - // Split the work scopes - const workScopes = parseListFromString(val.workScopes); - - // Mint certificate using contract - // NOTE: we set the times to be UNIX time (seconds since the epoch) - // but Date.getTime() returns milliseconds since the epoch - // NOTE: we are fixing the impactTimeStart to be the same as workTimeStart - const impactTimeframeStart = val.workTimeStart - ? new Date(val.workTimeStart).getTime() / 1000 - : 0; - /** - const impactTimeframeStart = val.impactTimeStart - ? new Date(val.impactTimeStart).getTime() / 1000 - : 0; - */ - const impactTimeframeEnd = - val.impactTimeEnd !== "indefinite" && val.impactTimeEnd !== undefined - ? new Date(val.impactTimeEnd).getTime() / 1000 - : 0; - const workTimeframeStart = val.workTimeStart - ? new Date(val.workTimeStart).getTime() / 1000 - : 0; - const workTimeframeEnd = val.workTimeEnd - ? new Date(val.workTimeEnd).getTime() / 1000 - : 0; - - let properties = []; - if (val.metadataProperties) { - try { - properties = JSON.parse(val.metadataProperties); - } catch (e) { - console.warn( - `Unable to parse metadataProperties: ${val.metadataProperties}`, - ); - } - } - - return formatHypercertData({ - name: val.name, - description: val.description, - external_url: val.externalLink, - image: image ?? "", - contributors: contributorNamesAndAddresses, - workTimeframeStart, - workTimeframeEnd, - impactTimeframeStart, - impactTimeframeEnd, - workScope: workScopes, - impactScope: val.impactScopes, - rights: val.rights, - version: DEFAULT_HYPERCERT_VERSION, - properties: properties, - excludedImpactScope: [], - excludedRights: [], - excludedWorkScope: [], - }); -}; - -const exportAsImage = async (id: string) => { - const el = document.getElementById(id); - if (!el) { - return; - } - const canvas = await html2canvas(el, { - logging: true, - backgroundColor: null, - //useCORS: true, - proxy: "https://cors-proxy.hypercerts.workers.dev/", - imageTimeout: 0, - }); - const image = canvas.toDataURL("image/png", 1.0); - return image; -}; diff --git a/frontend/components/contribution-blueprint-create.tsx b/frontend/components/contribution-blueprint-create.tsx deleted file mode 100644 index 8ca28c1a..00000000 --- a/frontend/components/contribution-blueprint-create.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { useListRegistries } from "../hooks/list-registries"; -import { Button, Divider } from "@mui/material"; -import { useState } from "react"; -import { AddRegistryDialog } from "./add-registry-dialog"; - -export const ContributionBlueprintCreate = () => { - const { data } = useListRegistries(); - const [showAddRegistryDialog, setShowAddRegistryDialog] = useState(false); - return ( -
- - {data?.map((registry: any) => ( -
-

{registry.name}

-

{registry.description}

-

{registry.owner_address}

- -
- ))} - setShowAddRegistryDialog(false)} - /> -
- ); -}; diff --git a/frontend/hooks/createBlueprintInRegistry.ts b/frontend/hooks/createBlueprintInRegistry.ts deleted file mode 100644 index d1d5d8d0..00000000 --- a/frontend/hooks/createBlueprintInRegistry.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { supabase } from "../lib/supabase-client"; -import { useMutation } from "wagmi"; - -export const useMintBlueprintToRegistry = ({ - onComplete, -}: { - onComplete?: () => void; -}) => { - return useMutation( - ["mintBlueprintToRegistry"], - async ({ - registryId, - minterAddress, - value, - }: { - registryId: string; - minterAddress: string; - value: string; - }) => { - supabase - .from("claim-blueprints-optimism") - .insert({ - registry_id: registryId, - minter_address: minterAddress, - form_values: value, - }) - .then((res) => { - if (res.error) { - throw res.error; - } - return true; - }); - }, - ); -}; diff --git a/frontend/plasmic-init.ts b/frontend/plasmic-init.ts index b8c4149f..a27ad396 100644 --- a/frontend/plasmic-init.ts +++ b/frontend/plasmic-init.ts @@ -1,9 +1,7 @@ -import { BlueprintCreateForm } from "./components/blueprint-create"; import { BurnFractionButton } from "./components/burn-fraction-button"; import ClaimAllFractionsButton from "./components/claim-all-fractions-button"; import { ClientGrid } from "./components/client-grid"; import { Config } from "./components/config"; -import { ContributionBlueprintCreate } from "./components/contribution-blueprint-create"; import { DEFAULT_TEST_DATA } from "./components/dapp-state"; import { FormField, @@ -238,29 +236,6 @@ PLASMIC.registerComponent(HypercertCreateForm, { importPath: "./components/hypercert-create", }); -PLASMIC.registerComponent(BlueprintCreateForm, { - name: "BlueprintCreateForm", - description: "Create a blueprint", - props: { - children: { - type: "slot", - defaultValue: { - type: "text", - value: "Placeholder", - }, - }, - }, - providesData: true, - importPath: "./components/blueprint-create", -}); - -PLASMIC.registerComponent(ContributionBlueprintCreate, { - name: "ContributionBlueprintCreate", - description: "Create a contribution blueprint", - importPath: "./components/contribution-blueprint-create", - props: {}, -}); - PLASMIC.registerComponent(FormError, { name: "FormError", description: "Displays the error associated with fieldName",