Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(launchpad): Add Collection Creation form front #1477

Open
wants to merge 9 commits into
base: main
Choose a base branch
from

Conversation

WaDadidou
Copy link
Collaborator

@WaDadidou WaDadidou commented Jan 5, 2025

more description could come soon...


Testable here: https://deploy-preview-1477--teritori-dapp.netlify.app/launchpad/apply (Teritori Testnet)


What's in this PR?

This PR is extracted from this one: #1024
It adds the Collection Creation form
image

The flow starts here:
<OmniLink
noHoverEffect
to={{ screen: "LaunchpadCreate" }}
style={{
flex: 1,
marginHorizontal: width >= MD_BREAKPOINT ? layout.spacing_x1_5 : 0,
marginVertical: width >= MD_BREAKPOINT ? 0 : layout.spacing_x1_5,
}}
>
<LargeBoxButton {...BUTTONS[1]} />
</OmniLink>

image

The flow ends here:
await nftLaunchpadContractClient.submitCollection({
collection,
});

image

I made this Zod object to pilot the Collection Creation form

export const ZodCollectionFormValues = z.object({
name: z.string().trim().min(1, DEFAULT_FORM_ERRORS.required),
description: z.string().trim().min(1, DEFAULT_FORM_ERRORS.required),
symbol: z
.string()
.trim()
.toUpperCase()
.min(1, DEFAULT_FORM_ERRORS.required)
.refine(
(value) => LETTERS_REGEXP.test(value),
DEFAULT_FORM_ERRORS.onlyLetters,
),
websiteLink: z
.string()
.trim()
.min(1, DEFAULT_FORM_ERRORS.required)
.refine((value) => URL_REGEX.test(value), DEFAULT_FORM_ERRORS.onlyUrl),
email: z
.string()
.trim()
.min(1, DEFAULT_FORM_ERRORS.required)
.refine((value) => EMAIL_REGEXP.test(value), DEFAULT_FORM_ERRORS.onlyEmail),
projectTypes: z.array(z.string().trim()).min(1, DEFAULT_FORM_ERRORS.required),
revealTime: z.number().optional(),
teamDescription: z.string().trim().min(1, DEFAULT_FORM_ERRORS.required),
partnersDescription: z.string().trim().min(1, DEFAULT_FORM_ERRORS.required),
investDescription: z.string().trim().min(1, DEFAULT_FORM_ERRORS.required),
investLink: z.string().trim().min(1, DEFAULT_FORM_ERRORS.required),
artworkDescription: z.string().trim().min(1, DEFAULT_FORM_ERRORS.required),
coverImage: ZodLocalFileData,
isPreviouslyApplied: z.boolean(),
isDerivativeProject: z.boolean(),
isReadyForMint: z.boolean(),
isDox: z.boolean(),
escrowMintProceedsPeriod: z
.string()
.trim()
.min(1, DEFAULT_FORM_ERRORS.required),
daoWhitelistCount: z
.string()
.trim()
.min(1, DEFAULT_FORM_ERRORS.required)
.refine(
(value) => NUMBERS_REGEXP.test(value),
DEFAULT_FORM_ERRORS.onlyNumbers,
),
mintPeriods: z.array(ZodCollectionMintPeriodFormValues).nonempty(),
royaltyAddress: z.string().trim().optional(),
royaltyPercentage: z
.string()
.trim()
.refine(
(value) => !value || NUMBERS_REGEXP.test(value),
DEFAULT_FORM_ERRORS.onlyNumbers,
)
.optional(),
assetsMetadatas: ZodCollectionAssetsMetadatasFormValues.optional(),
baseTokenUri: z
.string()
.trim()
.refine(
(value) => !value || isIpfsPathValid(value),
DEFAULT_FORM_ERRORS.onlyIpfsUri,
)
.optional(),
coverImageUri: z
.string()
.trim()
.refine(
(value) => !value || isIpfsPathValid(value),
DEFAULT_FORM_ERRORS.onlyIpfsUri,
)
.optional(),
});

The data will be send onchain using the nft-launchpad contract's submit_collection:

About Assets & Metadata

Use these files to test the step 6: test-files-step-6.zip
You will get these 2 warnings. It's expected, I wrongly filled the files to have these warnings as a demo.
So, you will get 3 images instead of 4 at the end.
You can fix the CSV files if you want, add assets, break the files, etc
image

The warnings don't block the flow, it just ignores the wrong assets.
The errors block the flow and ask the user to provide correct mapping files:
image

All these behaviors are handled here: https://github.com/TERITORI/teritori-dapp/blob/6ca68e031a528902d29ea9d559b466175de57902/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadAssetsAndMetadata/AssetsTab.tsx
I tried to well comment the code


What after this PR?

I want to separate the front in parts

  • Apply - Create Collection (This PR)
  • Apply - My Collections + Complete Collection
  • Admin - Overview
  • Admin - Collection Review

Possible enhancement that can be done in another PRs

We have to write a documentation

image
Especialy to guide the user on this step:
image
We must also provide two CSV templates: Attributes mapping file and an Assets mapping file

Copy link

netlify bot commented Jan 5, 2025

Deploy Preview for teritori-dapp ready!

Name Link
🔨 Latest commit cc921e8
🔍 Latest deploy log https://app.netlify.com/sites/teritori-dapp/deploys/67954da8711d860008acbabd
😎 Deploy Preview https://deploy-preview-1477--teritori-dapp.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify site configuration.

Copy link

netlify bot commented Jan 5, 2025

Deploy Preview for gno-dapp ready!

Name Link
🔨 Latest commit cc921e8
🔍 Latest deploy log https://app.netlify.com/sites/gno-dapp/deploys/67954da8ffafe00008b4b73a
😎 Deploy Preview https://deploy-preview-1477--gno-dapp.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify site configuration.

@WaDadidou WaDadidou force-pushed the feat-launchpad-submit-collection-front branch from 2bf16bf to 6ca68e0 Compare January 7, 2025 19:42
Copy link
Member

@MikaelVallenet MikaelVallenet left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Comment on lines 75 to 129
const hasErrors = (stepKey: number) => {
if (
(stepKey === 1 &&
(!!collectionForm.getFieldState("name").error ||
!!collectionForm.getFieldState("description").error ||
!!collectionForm.getFieldState("symbol").error)) ||
!!collectionForm.getFieldState("coverImage").error ||
!!collectionForm.getFieldState("assetsMetadatas.nftApiKey").error
) {
return true;
}
if (
stepKey === 2 &&
(!!collectionForm.getFieldState("websiteLink").error ||
!!collectionForm.getFieldState("isDerivativeProject").error ||
!!collectionForm.getFieldState("projectTypes").error ||
!!collectionForm.getFieldState("isPreviouslyApplied").error ||
!!collectionForm.getFieldState("email").error)
) {
return true;
}
if (
stepKey === 3 &&
(!!collectionForm.getFieldState("teamDescription").error ||
!!collectionForm.getFieldState("partnersDescription").error ||
!!collectionForm.getFieldState("investDescription").error ||
!!collectionForm.getFieldState("investLink").error)
) {
return true;
}
if (
stepKey === 4 &&
(!!collectionForm.getFieldState("artworkDescription").error ||
!!collectionForm.getFieldState("isReadyForMint").error ||
!!collectionForm.getFieldState("isDox").error ||
!!collectionForm.getFieldState("daoWhitelistCount").error ||
!!collectionForm.getFieldState("escrowMintProceedsPeriod").error)
) {
return true;
}
if (
stepKey === 5 &&
(!!collectionForm.getFieldState("mintPeriods").error ||
!!collectionForm.getFieldState("royaltyAddress").error ||
!!collectionForm.getFieldState("royaltyPercentage").error)
) {
return true;
}
if (
stepKey === 6 &&
!!collectionForm.getFieldState("assetsMetadatas").error
) {
return true;
}
};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do react hook form not give some methods to handle this cleaner ?
Or should we do a map[step]list of field key to just loop through the fields corresponding to the steps
like for_, val := map[step] { if !! colectionForm.getFieldState(val).error} kinda

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the return of useForm hook you have an object named formState and inside of it you have errors. I don't know what infos you have exactly but can be a solution.

const { formState: { errors } } = useForm()

But agree with @MikaelVallenet that this if forest is not very clean, and if you can't handle this with errors the map can be a better solution too

Copy link
Collaborator Author

@WaDadidou WaDadidou Jan 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The correct way could be to just control the formState.errors yes.
I don't remember why it didn't work, but I'm checking and I'll fix.
Yes, it's messy and we need to add/remove rows from here after add/remove a field from the form...
I didn't enhanced very much this part (Stepper), because thought about this issue (Creating a reusable form wizard): #1475

I'll just fix this comment for now

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed: c85f0f5

@MikaelVallenet
Copy link
Member

What is this about, i can choose between 1, 2 & 3

image

@MikaelVallenet
Copy link
Member

Why would they give us some whitelist ? should we incentivize it or maybe turn this like "Teritori develop these tools with ❤️, if you appreciate it you could contribute by giving some WL spot to our DAO" this way it does not look like "we would love to have some WL to share to our DAO members" but more like you can reward use for the dev. of these tools by giving some. wdyt
image

@MikaelVallenet
Copy link
Member

image
Would be nice to display the timezone to avoid misunderstandings

Copy link
Collaborator

@clegirar clegirar left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In general could you please use fontRegular instead of fontMedium or fontSemiBold ? I can do it if you want 👍

About using useFormContext i think my mind can change because when you call it, you specify the type of the form so i'm not confusing when you call it, and found it better to read, it's not a lot of additional abstraction.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This component is used once and it's almost the same than MultipleSelectInput maybe you can use MultipleSelectInput instead of create an other one ?

Copy link
Collaborator Author

@WaDadidou WaDadidou Jan 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think so.
NetworkSelectorWithLabel uses NetworkSelectorMenu and its onPressNetwork and filter logic.
NetworkSelectorWithLabel is closer to NetworkSelectorMenu than MultipleSelectInput an its dropdownOptions

Comment on lines 75 to 129
const hasErrors = (stepKey: number) => {
if (
(stepKey === 1 &&
(!!collectionForm.getFieldState("name").error ||
!!collectionForm.getFieldState("description").error ||
!!collectionForm.getFieldState("symbol").error)) ||
!!collectionForm.getFieldState("coverImage").error ||
!!collectionForm.getFieldState("assetsMetadatas.nftApiKey").error
) {
return true;
}
if (
stepKey === 2 &&
(!!collectionForm.getFieldState("websiteLink").error ||
!!collectionForm.getFieldState("isDerivativeProject").error ||
!!collectionForm.getFieldState("projectTypes").error ||
!!collectionForm.getFieldState("isPreviouslyApplied").error ||
!!collectionForm.getFieldState("email").error)
) {
return true;
}
if (
stepKey === 3 &&
(!!collectionForm.getFieldState("teamDescription").error ||
!!collectionForm.getFieldState("partnersDescription").error ||
!!collectionForm.getFieldState("investDescription").error ||
!!collectionForm.getFieldState("investLink").error)
) {
return true;
}
if (
stepKey === 4 &&
(!!collectionForm.getFieldState("artworkDescription").error ||
!!collectionForm.getFieldState("isReadyForMint").error ||
!!collectionForm.getFieldState("isDox").error ||
!!collectionForm.getFieldState("daoWhitelistCount").error ||
!!collectionForm.getFieldState("escrowMintProceedsPeriod").error)
) {
return true;
}
if (
stepKey === 5 &&
(!!collectionForm.getFieldState("mintPeriods").error ||
!!collectionForm.getFieldState("royaltyAddress").error ||
!!collectionForm.getFieldState("royaltyPercentage").error)
) {
return true;
}
if (
stepKey === 6 &&
!!collectionForm.getFieldState("assetsMetadatas").error
) {
return true;
}
};
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the return of useForm hook you have an object named formState and inside of it you have errors. I don't know what infos you have exactly but can be a solution.

const { formState: { errors } } = useForm()

But agree with @MikaelVallenet that this if forest is not very clean, and if you can't handle this with errors the map can be a better solution too

const { width: windowWidth } = useWindowDimensions();
const scrollViewRef = useRef<ScrollView>(null);
const isMobile = useIsMobile();
const collectionForm = useFormContext<CollectionFormValues>();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my refacto on react-hook-form, with norman we said to each other that don't use useFormContext to handle our form. I don't have final opinion actually on how to use it, but i just want to be consistent when we use it

Copy link
Collaborator Author

@WaDadidou WaDadidou Jan 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Idk.
I just mention this issue here, because we are in LaunchpadStepper scope: #1475 (Form wizard)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found this file too big.. Can't extract functions ?

Copy link
Collaborator Author

@WaDadidou WaDadidou Jan 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I strongly commented the code because it contains the mapping files warning/errors detection.. Maybe I can extract this logic yes

Copy link
Collaborator Author

@WaDadidou WaDadidou Jan 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't figure out how to split this component...
If I add more components or files with functions, it will add more props and files juggling and adds difficulty to read.
I think it's well organized and clear when you fold the rows in IDE :/

Do you ave advises ?

(I still exported ResetAllButton to another file: cc921e8)

image

Comment on lines +64 to +69
style={[
fontMedium14,
{
color: neutral77,
},
]}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can fit in one line

Copy link
Collaborator Author

@WaDadidou WaDadidou Jan 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As in a ton of lines in our code.
I think you should not take attention to that until we find a rule (lint + CI ?)
It could depend on the IDEs.
But we can lint by adding these lines in .eslintrc.js (These rules seem to work well):
"array-bracket-newline": ["error", "consistent"],
"object-curly-newline": ["error", "never"]

image

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah there is a lot of lines.
Okay cool maybe we could add that in a specific PR ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issue created: #1494

Comment on lines +74 to +78
fontMedium14,
{
color: primaryColor,
},
]}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here you can't use MultipleSelectInput ?

Copy link
Collaborator Author

@WaDadidou WaDadidou Jan 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, no, it's nor the same usage: MultipleSelectInput has Checkbox in a deep level, it's "multiple".
But...
We want to refacto this MultipleSelectInput, and add common parent(s) used in SelectInputLaunchpad and MultipleSelectInput (And extract them from NFT Launchpad, and use them in ERC20 Launchpad, etc),
But
Should we do that in this PR?

@clegirar
Copy link
Collaborator

clegirar commented Jan 8, 2025

Screenshot 2025-01-08 at 11 52 51
Select file button is not centered, found that weird

@WaDadidou
Copy link
Collaborator Author

WaDadidou commented Jan 23, 2025

About using useFormContext i think my mind can change because when you call it, you specify the type of the form so i'm not confusing when you call it, and found it better to read, it's not a lot of additional abstraction.

@clegirar Your mind, but not fully about FromProvider? What was the problem with FormProvider? I think about perfomance
cc @n0izn0iz

@n0izn0iz
Copy link
Collaborator

n0izn0iz commented Jan 23, 2025

I'm not too fond of the FormProvider because it goes against composability. What's the point of making components if they can only be used in a specific context. Contexts are good for global data but I don't see the point of using them locally

@WaDadidou
Copy link
Collaborator Author

WaDadidou commented Jan 23, 2025

I'm not too fond of the FormProvider because it goes against composability. What's the point of making components if they can only be used in a specific context. Contexts are good for global data but I don't see the point of using them locally

My main arg is just to have less props, I'm ok with yours.
Actually I don't use FormProvider only because I need it, so it's too much. It should not be a standard, but a solution for specific cases. With that in mind, I'm ok to remove its usage from this PR!

@WaDadidou
Copy link
Collaborator Author

WaDadidou commented Jan 23, 2025

Select file button is not centered, found that weird

Only in Firefox, wtf

Fixed: b01448e

@WaDadidou
Copy link
Collaborator Author

In general could you please use fontRegular instead of fontMedium or fontSemiBold ? I can do it if you want 👍

Done 4241965

@clegirar
Copy link
Collaborator

I don't have final opinions on react-hook-form, actually the only thing i found cool it's the zodResolver, but i found that the lib add some weird complexity.
But i prefer a way with specific props to have atomic components, than a context, for sure.
If you want to stay atomic, i think you can have a component Input and a component FormInput that handle specific props for a form.

@WaDadidou
Copy link
Collaborator Author

WaDadidou commented Jan 24, 2025

I don't have final opinions on react-hook-form, actually the only thing i found cool it's the zodResolver, but i found that the lib add some weird complexity. But i prefer a way with specific props to have atomic components, than a context, for sure. If you want to stay atomic, i think you can have a component Input and a component FormInput that handle specific props for a form.

Honestly, I'm really ok with removing react-hook-form.
But for now, I will just remove FormProvider usage from NFTLaunchpad front. I really want to merge that because I'm so done with this feature.
=> Done: 1cd87fb
After that, I'll be open to make a loooot of refactos and set up solid front patterns

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants