Skip to content

Commit 785cc56

Browse files
committed
add royalties and sales settings
1 parent 806d631 commit 785cc56

File tree

8 files changed

+226
-22
lines changed

8 files changed

+226
-22
lines changed

apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/assets/create/_common/file-preview.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { Img } from "@/components/blocks/Img";
22
import { fileToBlobUrl } from "@/lib/file-to-url";
3+
import { cn } from "@/lib/utils";
4+
import { ImageOffIcon } from "lucide-react";
35
import { useEffect, useState } from "react";
46
import type { ThirdwebClient } from "thirdweb";
57
import { MediaRenderer } from "thirdweb/react";
@@ -33,6 +35,14 @@ export function FilePreview(props: {
3335
props.srcOrFile instanceof File &&
3436
props.srcOrFile.type.startsWith("image/");
3537

38+
if (!objectUrl) {
39+
return (
40+
<div className={cn("flex items-center justify-center", props.className)}>
41+
<ImageOffIcon className="size-6 text-muted-foreground" />
42+
</div>
43+
);
44+
}
45+
3646
if (isImage) {
3747
return (
3848
<Img

apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/assets/create/nft/_common/form.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { UseFormReturn } from "react-hook-form";
2+
import { isAddress } from "thirdweb";
23
import * as z from "zod";
34
import { socialUrlsSchema } from "../../_common/schema";
45
import type { NFTMetadataWithPrice } from "../upload-nfts/batch-upload/process-files";
@@ -20,6 +21,7 @@ export type NFTCollectionInfoForm = UseFormReturn<NFTCollectionInfoFormValues>;
2021

2122
export type CreateNFTFormValues = NFTCollectionInfoFormValues & {
2223
nfts: NFTMetadataWithPrice[];
24+
sales: NFTSalesSettingsFormValues;
2325
};
2426

2527
export type CreateNFTFunctions = {
@@ -41,3 +43,21 @@ export type CreateNFTFunctions = {
4143
lazyMintNFTs: (values: CreateNFTFormValues) => Promise<void>;
4244
};
4345
};
46+
47+
const addressSchema = z.string().refine((value) => {
48+
if (isAddress(value)) {
49+
return true;
50+
}
51+
52+
return false;
53+
});
54+
55+
export const nftSalesSettingsFormSchema = z.object({
56+
royaltyRecipient: addressSchema,
57+
primarySaleRecipient: addressSchema,
58+
royaltyBps: z.coerce.number().min(0).max(10000),
59+
});
60+
61+
export type NFTSalesSettingsFormValues = z.infer<
62+
typeof nftSalesSettingsFormSchema
63+
>;

apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/assets/create/nft/create-nft-page-ui.tsx

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,17 @@ import {
88
type ThirdwebClient,
99
getAddress,
1010
} from "thirdweb";
11+
import { useActiveAccount } from "thirdweb/react";
1112
import {
1213
type CreateNFTFunctions,
1314
type NFTCollectionInfoFormValues,
15+
type NFTSalesSettingsFormValues,
1416
nftCollectionInfoFormSchema,
17+
nftSalesSettingsFormSchema,
1518
} from "./_common/form";
1619
import { NFTCollectionInfoFieldset } from "./collection-info/nft-collection-info-fieldset";
1720
import { LaunchNFT } from "./launch/launch-nft";
21+
import { SalesSettings } from "./sales/sales-settings";
1822
import type {} from "./upload-nfts/batch-upload/process-files";
1923
import { type NFTData, UploadNFTsFieldset } from "./upload-nfts/upload-nfts";
2024

@@ -27,14 +31,25 @@ export function CreateNFTPageUI(props: {
2731
projectSlug: string;
2832
}) {
2933
const [step, setStep] = useState<
30-
"collection-info" | "upload-assets" | "launch"
34+
"collection-info" | "upload-assets" | "launch" | "sales-settings"
3135
>("collection-info");
3236

37+
const activeAccount = useActiveAccount();
38+
3339
const [nftData, setNFTData] = useState<NFTData>({
3440
type: "single",
3541
nft: null,
3642
});
3743

44+
const nftSalesSettingsForm = useForm<NFTSalesSettingsFormValues>({
45+
resolver: zodResolver(nftSalesSettingsFormSchema),
46+
defaultValues: {
47+
royaltyRecipient: activeAccount?.address || "",
48+
primarySaleRecipient: activeAccount?.address || "",
49+
royaltyBps: 0,
50+
},
51+
});
52+
3853
const nftCollectionInfoForm = useNFTCollectionInfoForm();
3954

4055
return (
@@ -81,7 +96,7 @@ export function CreateNFTPageUI(props: {
8196
nftData={nftData}
8297
setNFTData={setNFTData}
8398
onNext={() => {
84-
setStep("launch");
99+
setStep("sales-settings");
85100
}}
86101
onPrev={() => {
87102
setStep("collection-info");
@@ -91,10 +106,24 @@ export function CreateNFTPageUI(props: {
91106
/>
92107
)}
93108

109+
{step === "sales-settings" && (
110+
<SalesSettings
111+
form={nftSalesSettingsForm}
112+
client={props.client}
113+
onNext={() => {
114+
setStep("launch");
115+
}}
116+
onPrev={() => {
117+
setStep("upload-assets");
118+
}}
119+
/>
120+
)}
121+
94122
{step === "launch" && (
95123
<LaunchNFT
96124
values={{
97125
...nftCollectionInfoForm.watch(),
126+
sales: nftSalesSettingsForm.watch(),
98127
nfts:
99128
nftData.type === "multiple"
100129
? nftData.nfts?.type === "data"
@@ -105,7 +134,7 @@ export function CreateNFTPageUI(props: {
105134
: [],
106135
}}
107136
onPrevious={() => {
108-
setStep("upload-assets");
137+
setStep("sales-settings");
109138
}}
110139
projectSlug={props.projectSlug}
111140
client={props.client}

apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/assets/create/nft/create-nft-page.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -121,8 +121,9 @@ export function CreateNFTPage(props: {
121121
description: formValues.description,
122122
image: formValues.image,
123123
social_urls: transformSocialUrls(formValues.socialUrls),
124-
// TODO Royalty stuff
125-
// TODO Sales recipient
124+
saleRecipient: formValues.sales.primarySaleRecipient,
125+
royaltyRecipient: formValues.sales.royaltyRecipient,
126+
royaltyBps: BigInt(formValues.sales.royaltyBps),
126127
},
127128
});
128129
} else {
@@ -137,8 +138,9 @@ export function CreateNFTPage(props: {
137138
description: formValues.description,
138139
image: formValues.image,
139140
social_urls: transformSocialUrls(formValues.socialUrls),
140-
// TODO Royalty stuff
141-
// TODO Sales recipient
141+
saleRecipient: formValues.sales.primarySaleRecipient,
142+
royaltyRecipient: formValues.sales.royaltyRecipient,
143+
royaltyBps: BigInt(formValues.sales.royaltyBps),
142144
},
143145
});
144146
}

apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/assets/create/nft/launch/launch-nft.tsx

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import { MultiStepStatus } from "@/components/blocks/multi-step-status/multi-step-status";
44
import type { MultiStepState } from "@/components/blocks/multi-step-status/multi-step-status";
5+
import { WalletAddress } from "@/components/blocks/wallet-address";
56
import { Badge } from "@/components/ui/badge";
67
import { Button } from "@/components/ui/button";
78
import {
@@ -109,12 +110,15 @@ export function LaunchNFT(props: {
109110
status: { type: "idle" },
110111
},
111112
{
112-
label: "Mint NFTs",
113+
label: formValues.nfts.length > 1 ? "Mint NFTs" : "Mint NFT",
113114
id: stepIds["mint-nfts"],
114115
status: { type: "idle" },
115116
},
116117
{
117-
label: "Set claim conditions",
118+
label:
119+
formValues.nfts.length > 1
120+
? "Set claim conditions"
121+
: "Set claim condition",
118122
id: stepIds["set-claim-conditions"],
119123
status: { type: "idle" },
120124
},
@@ -133,10 +137,6 @@ export function LaunchNFT(props: {
133137
: 1;
134138

135139
const ercType: "erc721" | "erc1155" = useMemo(() => {
136-
if (formValues.nfts.length === 1) {
137-
return "erc721";
138-
}
139-
140140
// if all prices (amount + currency) are same and all supply is to 1
141141
const shouldDeployERC721 = formValues.nfts.every((nft) => {
142142
return (
@@ -358,7 +358,7 @@ export function LaunchNFT(props: {
358358

359359
{/* Token info */}
360360
<div className="border-b border-dashed px-4 py-6 pb-6 md:px-6 ">
361-
<h2 className="mb-3 font-medium text-sm">Collection Info</h2>
361+
<h2 className="mb-3 font-semibold text-base">Collection Info</h2>
362362

363363
<div className="flex flex-col gap-6 lg:flex-row">
364364
<OverviewField name="Image" className="shrink-0">
@@ -403,8 +403,8 @@ export function LaunchNFT(props: {
403403
</div>
404404
</div>
405405

406-
<div className="px-4 py-6 pb-6 md:px-6">
407-
<h2 className="font-medium text-sm">NFTs</h2>
406+
<div className="border-b border-dashed px-4 py-6 pb-6 md:px-6">
407+
<h2 className="font-semibold text-base">NFTs</h2>
408408
<div className="mt-3 flex flex-col gap-4 lg:flex-row lg:gap-4 lg:[&>*:not(:first-child)]:border-l lg:[&>*:not(:first-child)]:border-dashed lg:[&>*:not(:first-child)]:pl-5">
409409
<OverviewField name="Total NFTs">
410410
<OverviewFieldValue value={formValues.nfts.length.toString()} />
@@ -443,6 +443,35 @@ export function LaunchNFT(props: {
443443
)}
444444
</div>
445445
</div>
446+
447+
<div className="px-4 py-6 pb-6 md:px-6">
448+
<h2 className="font-semibold text-base">Sales and Fees</h2>
449+
<div className="mt-3 flex flex-col gap-4 lg:flex-row lg:gap-4 lg:[&>*:not(:first-child)]:border-l lg:[&>*:not(:first-child)]:border-dashed lg:[&>*:not(:first-child)]:pl-5">
450+
<OverviewField name="Primary Sales Recipient">
451+
<WalletAddress
452+
address={formValues.sales.primarySaleRecipient}
453+
client={props.client}
454+
className="h-auto py-1"
455+
iconClassName="size-3.5"
456+
/>
457+
</OverviewField>
458+
459+
<OverviewField name="Royalties Recipient">
460+
<WalletAddress
461+
address={formValues.sales.royaltyRecipient}
462+
client={props.client}
463+
iconClassName="size-3.5"
464+
className="h-auto py-1"
465+
/>
466+
</OverviewField>
467+
468+
<OverviewField name="Royalties">
469+
<p className="flex items-center gap-1 text-foreground text-sm">
470+
{Number(formValues.sales.royaltyBps) / 100}%
471+
</p>
472+
</OverviewField>
473+
</div>
474+
</div>
446475
</StepCard>
447476
);
448477
}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import { FormFieldSetup } from "@/components/blocks/FormFieldSetup";
2+
import { Form } from "@/components/ui/form";
3+
import { BasisPointsInput } from "components/inputs/BasisPointsInput";
4+
import type { UseFormReturn } from "react-hook-form";
5+
import type { ThirdwebClient } from "thirdweb";
6+
import { SolidityInput } from "../../../../../../../../../../contract-ui/components/solidity-inputs";
7+
import { StepCard } from "../../_common/step-card";
8+
import type { NFTSalesSettingsFormValues } from "../_common/form";
9+
10+
export function SalesSettings(props: {
11+
onNext: () => void;
12+
onPrev: () => void;
13+
form: UseFormReturn<NFTSalesSettingsFormValues>;
14+
client: ThirdwebClient;
15+
}) {
16+
const errors = props.form.formState.errors;
17+
const bpsNumValue = props.form.watch("royaltyBps");
18+
19+
return (
20+
<Form {...props.form}>
21+
<form
22+
onSubmit={props.form.handleSubmit(() => {
23+
props.onNext();
24+
})}
25+
>
26+
<StepCard
27+
tracking={{
28+
page: "sales-settings",
29+
contractType: "NFTCollection",
30+
}}
31+
title="Sales and Fees"
32+
prevButton={{
33+
onClick: props.onPrev,
34+
}}
35+
nextButton={{
36+
type: "submit",
37+
}}
38+
>
39+
<div className="px-4 py-6 md:px-6">
40+
<div className="mb-4 space-y-1">
41+
<h3 className="font-semibold text-base"> Primary Sales</h3>
42+
<p className="text-muted-foreground text-sm">
43+
Set the wallet address that should receive the revenue from
44+
initial sales of the assets
45+
</p>
46+
</div>
47+
<FormFieldSetup
48+
className="grow"
49+
isRequired
50+
label="Primary Sales Recipient"
51+
errorMessage={errors.primarySaleRecipient?.message}
52+
>
53+
<SolidityInput
54+
solidityType="address"
55+
className="bg-background"
56+
{...props.form.register("primarySaleRecipient")}
57+
client={props.client}
58+
/>
59+
</FormFieldSetup>
60+
</div>
61+
62+
<div className="border-t border-dashed px-4 py-6 md:px-6">
63+
<div className="mb-4 space-y-1">
64+
<h3 className="font-semibold text-base">Royalties</h3>
65+
<p className="text-muted-foreground text-sm">
66+
Set the wallet address should receive the revenue from royalties
67+
earned from secondary sales of the assets
68+
</p>
69+
</div>
70+
<div className="flex flex-col gap-4 md:flex-row">
71+
<FormFieldSetup
72+
className="grow"
73+
isRequired
74+
label="Recipient Address"
75+
errorMessage={errors.royaltyRecipient?.message}
76+
helperText={
77+
<>
78+
The wallet address that should receive the revenue from
79+
royalties earned from secondary sales of the assets.
80+
</>
81+
}
82+
>
83+
<SolidityInput
84+
solidityType="address"
85+
className="bg-background"
86+
{...props.form.register("royaltyRecipient")}
87+
client={props.client}
88+
/>
89+
</FormFieldSetup>
90+
91+
<FormFieldSetup
92+
isRequired
93+
label="Percentage"
94+
className="shrink-0 md:max-w-[150px]"
95+
errorMessage={errors.royaltyBps?.message}
96+
>
97+
<BasisPointsInput
98+
className="bg-background"
99+
value={bpsNumValue}
100+
onChange={(value) => props.form.setValue("royaltyBps", value)}
101+
defaultValue={0}
102+
/>
103+
</FormFieldSetup>
104+
</div>
105+
</div>
106+
</StepCard>
107+
</form>
108+
</Form>
109+
);
110+
}

apps/dashboard/src/components/inputs/BasisPointsInput.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
/* eslint-disable no-restricted-syntax */
22
import { Input } from "@/components/ui/input";
3+
import { cn } from "@/lib/utils";
34
import { PercentIcon } from "lucide-react";
45
import { useEffect, useState } from "react";
56

@@ -44,9 +45,12 @@ export const BasisPointsInput: React.FC<BasisPointsInputProps> = ({
4445
return (
4546
<div className="flex overflow-hidden rounded-lg border border-border ring-offset-background focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2">
4647
<Input
47-
className="rounded-r-none border-none focus-visible:ring-0 focus-visible:ring-offset-0"
4848
value={stringValue}
4949
{...restInputProps}
50+
className={cn(
51+
"rounded-r-none border-none focus-visible:ring-0 focus-visible:ring-offset-0",
52+
restInputProps.className,
53+
)}
5054
onChange={(e) => setStringValue(e.target.value)}
5155
onBlur={(e) => {
5256
const val = e.target.value;

0 commit comments

Comments
 (0)