diff --git a/.changeset/better-owls-flash.md b/.changeset/better-owls-flash.md new file mode 100644 index 00000000000..13f692e7ca9 --- /dev/null +++ b/.changeset/better-owls-flash.md @@ -0,0 +1,6 @@ +--- +"thirdweb": patch +--- + +- Add support for blob urls in `MediaRenderer` component +- Fix `className` prop not set in loading state of `MediaRenderer` component \ No newline at end of file diff --git a/apps/dashboard/src/@/components/blocks/Img.tsx b/apps/dashboard/src/@/components/blocks/Img.tsx index 05af128c490..e1fcc37142a 100644 --- a/apps/dashboard/src/@/components/blocks/Img.tsx +++ b/apps/dashboard/src/@/components/blocks/Img.tsx @@ -11,6 +11,7 @@ type imgElementProps = React.DetailedHTMLProps< skeleton?: React.ReactNode; fallback?: React.ReactNode; src: string | undefined; + containerClassName?: string; }; export function Img(props: imgElementProps) { @@ -23,7 +24,8 @@ export function Img(props: imgElementProps) { : props.src === "" ? "fallback" : _status; - const { className, fallback, skeleton, ...restProps } = props; + const { className, fallback, skeleton, containerClassName, ...restProps } = + props; const defaultSkeleton =
; const defaultFallback =
; const imgRef = useRef(null); @@ -47,7 +49,7 @@ export function Img(props: imgElementProps) { }, []); return ( -
+
option.value === selectedValue) + ) { + options.push({ + label: props.selectedToken?.address || "Unknown", + value: selectedValue, + }); + } + return ( ( +
+ +
+ ), + ], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + isError: false, + onDrop: () => {}, + title: "This is a title", + description: "This is a description for drop zone", + accept: undefined, + resetButton: undefined, + }, +}; + +export const ErrorState: Story = { + args: { + isError: true, + onDrop: () => {}, + title: "this is title", + description: "This is a description", + accept: undefined, + resetButton: undefined, + }, +}; + +export const ErrorStateWithResetButton: Story = { + args: { + isError: true, + onDrop: () => {}, + title: "this is title", + description: "This is a description", + accept: undefined, + resetButton: { + label: "Remove Files", + onClick: () => {}, + }, + }, +}; diff --git a/apps/dashboard/src/@/components/blocks/drop-zone/drop-zone.tsx b/apps/dashboard/src/@/components/blocks/drop-zone/drop-zone.tsx new file mode 100644 index 00000000000..4bc7612177e --- /dev/null +++ b/apps/dashboard/src/@/components/blocks/drop-zone/drop-zone.tsx @@ -0,0 +1,81 @@ +import { Button } from "@/components/ui/button"; +import { cn } from "@/lib/utils"; +import { UploadIcon, XIcon } from "lucide-react"; +import { useDropzone } from "react-dropzone"; + +export function DropZone(props: { + isError: boolean; + onDrop: (acceptedFiles: File[]) => void; + title: string; + description: string; + resetButton: + | { + label: string; + onClick: () => void; + } + | undefined; + className?: string; + accept: string | undefined; +}) { + const { getRootProps, getInputProps } = useDropzone({ + onDrop: props.onDrop, + }); + + const { resetButton } = props; + + return ( +
+ +
+ {!props.isError && ( +
+
+ +
+

+ {props.title} +

+

+ {props.description} +

+
+ )} + + {props.isError && ( +
+
+ +
+

+ {props.title} +

+

+ {props.description} +

+ + {resetButton && ( + + )} +
+ )} +
+
+ ); +} diff --git a/apps/dashboard/src/@/components/blocks/multi-step-status/multi-step-status.stories.tsx b/apps/dashboard/src/@/components/blocks/multi-step-status/multi-step-status.stories.tsx index 3dbe5165115..e7698c97641 100644 --- a/apps/dashboard/src/@/components/blocks/multi-step-status/multi-step-status.stories.tsx +++ b/apps/dashboard/src/@/components/blocks/multi-step-status/multi-step-status.stories.tsx @@ -16,45 +16,34 @@ const meta = { export default meta; type Story = StoryObj; -const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); - export const AllStates: Story = { args: { + onRetry: () => {}, steps: [ { status: { type: "completed" }, label: "Connect Wallet", - execute: async () => { - await sleep(1000); - }, + id: "connect-wallet", }, { status: { type: "pending" }, label: "Sign Message", - execute: async () => { - await sleep(1000); - }, + id: "sign-message", }, { status: { type: "error", message: "This is an error message" }, label: "Approve Transaction", - execute: async () => { - await sleep(1000); - }, + id: "approve-transaction", }, { status: { type: "idle" }, label: "Confirm Transaction", - execute: async () => { - await sleep(1000); - }, + id: "confirm-transaction", }, { status: { type: "idle" }, label: "Finalize", - execute: async () => { - await sleep(1000); - }, + id: "finalize", }, ], }, diff --git a/apps/dashboard/src/@/components/blocks/multi-step-status/multi-step-status.tsx b/apps/dashboard/src/@/components/blocks/multi-step-status/multi-step-status.tsx index 7779def1757..3f56c48c287 100644 --- a/apps/dashboard/src/@/components/blocks/multi-step-status/multi-step-status.tsx +++ b/apps/dashboard/src/@/components/blocks/multi-step-status/multi-step-status.tsx @@ -10,21 +10,23 @@ import { import { DynamicHeight } from "../../ui/DynamicHeight"; import { Spinner } from "../../ui/Spinner/Spinner"; -export type MultiStepState = { +export type MultiStepState = { + id: T; status: | { type: "idle" | "pending" | "completed"; } | { type: "error"; - message: string | React.ReactNode; + message: React.ReactNode; }; label: string; - execute: () => Promise; + description?: string; }; -export function MultiStepStatus(props: { - steps: MultiStepState[]; +export function MultiStepStatus(props: { + steps: MultiStepState[]; + onRetry: (step: MultiStepState) => void; }) { return ( @@ -55,6 +57,15 @@ export function MultiStepStatus(props: { {step.label}

+ {/* show description when this step is active */} + {(step.status.type === "pending" || + step.status.type === "error") && + step.description && ( +

+ {step.description} +

+ )} + {step.status.type === "error" && (

@@ -64,7 +75,7 @@ export function MultiStepStatus(props: { variant="destructive" size="sm" className="gap-2" - onClick={() => step.execute()} + onClick={() => props.onRetry(step)} > Retry diff --git a/apps/dashboard/src/@/components/ui/decimal-input.tsx b/apps/dashboard/src/@/components/ui/decimal-input.tsx index ee942675c15..ed3b7be6591 100644 --- a/apps/dashboard/src/@/components/ui/decimal-input.tsx +++ b/apps/dashboard/src/@/components/ui/decimal-input.tsx @@ -6,6 +6,7 @@ export function DecimalInput(props: { id?: string; className?: string; placeholder?: string; + disabled?: boolean; }) { return ( { const number = Number(e.target.value); // ignore if string becomes invalid number diff --git a/apps/dashboard/src/@/lib/file-to-url.ts b/apps/dashboard/src/@/lib/file-to-url.ts new file mode 100644 index 00000000000..8c08065cba6 --- /dev/null +++ b/apps/dashboard/src/@/lib/file-to-url.ts @@ -0,0 +1,4 @@ +export function fileToBlobUrl(file: File) { + const blob = new Blob([file], { type: file.type }); + return URL.createObjectURL(blob); +} diff --git a/apps/dashboard/src/app/(app)/(dashboard)/(bridge)/routes/page.tsx b/apps/dashboard/src/app/(app)/(dashboard)/(bridge)/routes/page.tsx index a0d78cfe929..6b9345594e6 100644 --- a/apps/dashboard/src/app/(app)/(dashboard)/(bridge)/routes/page.tsx +++ b/apps/dashboard/src/app/(app)/(dashboard)/(bridge)/routes/page.tsx @@ -1,7 +1,7 @@ +import { getAuthToken } from "@app/api/lib/getAuthToken"; import { ArrowUpRightIcon } from "lucide-react"; import type { Metadata } from "next"; import { headers } from "next/headers"; -import { getAuthToken } from "../../../api/lib/getAuthToken"; import { SearchInput } from "./components/client/search"; import { QueryType } from "./components/client/type"; import { RouteListView } from "./components/client/view"; diff --git a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/(chainPage)/components/client/FaucetButton.tsx b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/(chainPage)/components/client/FaucetButton.tsx index 9a6175bf37d..5a0355cc569 100644 --- a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/(chainPage)/components/client/FaucetButton.tsx +++ b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/(chainPage)/components/client/FaucetButton.tsx @@ -49,6 +49,7 @@ import { useSwitchActiveWalletChain, useWalletBalance, } from "thirdweb/react"; +import { parseError } from "utils/errorParser"; import { z } from "zod"; function formatTime(seconds: number) { @@ -234,7 +235,12 @@ export function FaucetButton({ const claimPromise = claimMutation.mutateAsync(values.turnstileToken); toast.promise(claimPromise, { success: `${amount} ${chain.nativeCurrency.symbol} sent successfully`, - error: `Failed to claim ${amount} ${chain.nativeCurrency.symbol}`, + error: (err) => { + return { + message: `Failed to claim ${amount} ${chain.nativeCurrency.symbol}`, + description: parseError(err), + }; + }, }); }; diff --git a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/(chainPage)/layout.tsx b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/(chainPage)/layout.tsx index 5c654e0d9b7..21a7d2b0f43 100644 --- a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/(chainPage)/layout.tsx +++ b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/(chainPage)/layout.tsx @@ -14,16 +14,16 @@ import { DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { getClientThirdwebClient } from "@/constants/thirdweb-client.client"; +import { + getAuthToken, + getAuthTokenWalletAddress, +} from "@app/api/lib/getAuthToken"; import { ChevronDownIcon, TicketCheckIcon } from "lucide-react"; import type { Metadata } from "next"; import Link from "next/link"; import { redirect } from "next/navigation"; import { mapV4ChainToV5Chain } from "../../../../../../contexts/map-chains"; import { NebulaChatButton } from "../../../../../nebula-app/(app)/components/FloatingChat/FloatingChat"; -import { - getAuthToken, - getAuthTokenWalletAddress, -} from "../../../../api/lib/getAuthToken"; import { TeamHeader } from "../../../../team/components/TeamHeader/team-header"; import { StarButton } from "../../components/client/star-button"; import { getChain, getChainMetadata } from "../../utils"; diff --git a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/snapshot-upload.tsx b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/snapshot-upload.tsx index 55cb315ea80..49a7b6ab908 100644 --- a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/snapshot-upload.tsx +++ b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/snapshot-upload.tsx @@ -13,6 +13,7 @@ import { cn } from "@/lib/utils"; import { useCsvUpload } from "hooks/useCsvUpload"; import { CircleAlertIcon, DownloadIcon, UploadIcon } from "lucide-react"; import { useRef } from "react"; +import { useDropzone } from "react-dropzone"; import type { Column } from "react-table"; import { type ThirdwebClient, ZERO_ADDRESS } from "thirdweb"; import { CsvDataTable } from "../csv-data-table"; @@ -52,23 +53,18 @@ const SnapshotViewerSheetContent: React.FC = ({ onClose, client, }) => { - const { - normalizeQuery, - getInputProps, - getRootProps, - isDragActive, - rawData, - noCsv, - reset, - removeInvalid, - } = useCsvUpload({ + const csvUpload = useCsvUpload({ csvParser, defaultRawData: value, client, }); + const dropzone = useDropzone({ + onDrop: csvUpload.setFiles, + }); + const paginationPortalRef = useRef(null); - const normalizeData = normalizeQuery.data; + const normalizeData = csvUpload.normalizeQuery.data; if (!normalizeData) { return ( @@ -152,11 +148,11 @@ const SnapshotViewerSheetContent: React.FC = ({ return (

- {rawData.length > 0 ? ( + {csvUpload.rawData.length > 0 ? (
portalRef={paginationPortalRef} - data={normalizeQuery.data.result} + data={csvUpload.normalizeQuery.data.result} columns={columns} />
@@ -166,23 +162,27 @@ const SnapshotViewerSheetContent: React.FC = ({
- +
- {isDragActive ? ( + {dropzone.isDragActive ? (

Drop the files here

) : ( -

- {noCsv +

+ {csvUpload.noCsv ? `No valid CSV file found, make sure your CSV includes the "address" column.` : "Drag & Drop a CSV file here"}

@@ -294,20 +294,20 @@ const SnapshotViewerSheetContent: React.FC = ({
- {normalizeQuery.data?.invalidFound ? ( + {csvUpload.normalizeQuery.data?.invalidFound ? ( diff --git a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/layout.tsx b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/layout.tsx index cf1753a6f2a..5f75485f9b6 100644 --- a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/layout.tsx +++ b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/layout.tsx @@ -1,5 +1,5 @@ +import { getAuthToken } from "@app/api/lib/getAuthToken"; import type { Metadata } from "next"; -import { getAuthToken } from "../../../../api/lib/getAuthToken"; import { SharedContractLayout, generateContractLayoutMetadata, diff --git a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/components/BatchMetadata.tsx b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/components/BatchMetadata.tsx index 621b03ce0e6..4402b162f59 100644 --- a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/components/BatchMetadata.tsx +++ b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/components/BatchMetadata.tsx @@ -199,7 +199,11 @@ function UploadMetadataNFTSection(props: {
{/* Left */}
- +
{/* Right */} diff --git a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/components/Mintable.tsx b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/components/Mintable.tsx index a2d9b40b26c..3b5d812b91b 100644 --- a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/components/Mintable.tsx +++ b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/components/Mintable.tsx @@ -397,7 +397,11 @@ function MintNFTSection(props: {
{/* Left */}
- +
{/* Right */} diff --git a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/components/nft/NFTMediaFormGroup.tsx b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/components/nft/NFTMediaFormGroup.tsx index a9a5a1bc7f2..9b4d4ac9c24 100644 --- a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/components/nft/NFTMediaFormGroup.tsx +++ b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/components/nft/NFTMediaFormGroup.tsx @@ -1,89 +1,37 @@ "use client"; import { FormFieldSetup } from "@/components/blocks/FormFieldSetup"; import { FileInput } from "components/shared/FileInput"; -import { useImageFileOrUrl } from "hooks/useImageFileOrUrl"; import type { UseFormReturn } from "react-hook-form"; +import type { ThirdwebClient } from "thirdweb"; import type { NFTInput } from "thirdweb/utils"; +import { + getUploadedNFTMediaMeta, + handleNFTMediaUpload, +} from "./handleNFTMediaUpload"; type NFTMediaFormGroupValues = { image?: NFTInput["image"]; animation_url?: NFTInput["animation_url"]; external_url?: NFTInput["external_url"]; }; + export function NFTMediaFormGroup(props: { form: UseFormReturn; previewMaxWidth?: string; + client: ThirdwebClient; }) { // T contains all properties of NFTMediaFormGroupValues, so this is a-ok const form = props.form as unknown as UseFormReturn; - const setFile = (file: File) => { - const external_url = form.watch("external_url"); - const animation_url = form.watch("animation_url"); - - if (file.type.includes("image")) { - form.setValue("image", file); - if (external_url instanceof File) { - form.setValue("external_url", undefined); - } - if (animation_url instanceof File) { - form.setValue("animation_url", undefined); - } - } else if ( - ["audio", "video", "text/html", "model/*"].some((type: string) => - file.type.includes(type), - ) || - file.name?.endsWith(".glb") || - file.name?.endsWith(".usdz") || - file.name?.endsWith(".gltf") || - file.name.endsWith(".obj") - ) { - // audio, video, html, and glb (3d) files - form.setValue("animation_url", file); - if (external_url instanceof File) { - form.setValue("external_url", undefined); - } - } else if ( - ["text", "application/pdf"].some((type: string) => - file.type?.includes(type), - ) - ) { - // text and pdf files - form.setValue("external_url", file); - if (animation_url instanceof File) { - form.setValue("animation_url", undefined); - } - } - }; - - const external_url = form.watch("external_url"); - const animation_url = form.watch("animation_url"); - const image = form.watch("image"); - const errors = form.formState.errors; + const { media, image, mediaFileError, showCoverImageUpload } = + getUploadedNFTMediaMeta(form); - const imageUrl = useImageFileOrUrl(image as File | string); - const showCoverImageUpload = - animation_url instanceof File || external_url instanceof File; - - const mediaFileUrl = - animation_url instanceof File - ? animation_url - : external_url instanceof File - ? external_url - : image instanceof File - ? imageUrl - : undefined; + const previewMaxWidth = props.previewMaxWidth ?? "200px"; - const mediaFileError = - animation_url instanceof File - ? errors?.animation_url - : external_url instanceof File - ? errors?.external_url - : image instanceof File - ? errors?.image - : undefined; + const setFile = (file: File) => { + handleNFTMediaUpload({ file, form }); + }; - const previewMaxWidth = props.previewMaxWidth ?? "200px"; return (
(props: {

@@ -112,12 +61,13 @@ export function NFTMediaFormGroup(props: { htmlFor="cover-image" label="Cover Image" tooltip="You can optionally upload an image as the cover of your NFT." - isRequired + isRequired={false} > { form.setValue("image", file); diff --git a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/components/nft/handleNFTMediaUpload.ts b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/components/nft/handleNFTMediaUpload.ts new file mode 100644 index 00000000000..774de5b24d0 --- /dev/null +++ b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/components/nft/handleNFTMediaUpload.ts @@ -0,0 +1,85 @@ +import type { UseFormReturn } from "react-hook-form"; +import type { NFTInput } from "thirdweb/utils"; + +type MinimalNFTMetadata = { + image?: NFTInput["image"]; + animation_url?: NFTInput["animation_url"]; + external_url?: NFTInput["external_url"]; +}; + +export function handleNFTMediaUpload(params: { + file: File; + form: UseFormReturn; +}) { + const { file } = params; + const form = params.form as unknown as UseFormReturn; + + const external_url = form.getValues("external_url"); + const animation_url = form.getValues("animation_url"); + + if (file.type.includes("image")) { + form.setValue("image", file); + + if (external_url instanceof File) { + form.setValue("external_url", undefined); + } + + if (animation_url instanceof File) { + form.setValue("animation_url", undefined); + } + } else if ( + ["audio", "video", "text/html", "model/"].some((type: string) => + file.type.includes(type), + ) || + file.name?.endsWith(".glb") || + file.name?.endsWith(".usdz") || + file.name?.endsWith(".gltf") || + file.name.endsWith(".obj") + ) { + // audio, video, html, and glb (3d) files + form.setValue("animation_url", file); + if (external_url instanceof File) { + form.setValue("external_url", undefined); + } + } else if ( + ["text", "application/pdf"].some((type: string) => + file.type?.includes(type), + ) + ) { + // text and pdf files + form.setValue("external_url", file); + if (animation_url instanceof File) { + form.setValue("animation_url", undefined); + } + } +} + +export function getUploadedNFTMediaMeta( + _form: UseFormReturn, +) { + const form = _form as unknown as UseFormReturn; + + const _animation_url = form.watch("animation_url"); + const _external_url = form.watch("external_url"); + const _image = form.watch("image"); + const _media = _animation_url || _external_url || _image; + const errors = form.formState.errors; + + return { + media: stringOrFile(_media), + image: stringOrFile(_image), + mediaFileError: + errors?.animation_url || errors.external_url || errors?.image, + showCoverImageUpload: !!_animation_url || !!_external_url, + animation_url: stringOrFile(_animation_url), + external_url: stringOrFile(_external_url), + }; +} + +function stringOrFile(value: unknown): string | File | undefined { + if (typeof value === "string" || value instanceof File) { + return value; + } + + return undefined; +} diff --git a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/nfts/[tokenId]/components/claim-tab.tsx b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/nfts/[tokenId]/components/claim-tab.tsx index a3e6da385a7..db79fcd8e42 100644 --- a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/nfts/[tokenId]/components/claim-tab.tsx +++ b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/nfts/[tokenId]/components/claim-tab.tsx @@ -10,6 +10,7 @@ import { getApprovalForTransaction } from "thirdweb/extensions/erc20"; import { claimTo } from "thirdweb/extensions/erc1155"; import { useActiveAccount, useSendAndConfirmTransaction } from "thirdweb/react"; import { FormErrorMessage, FormHelperText, FormLabel } from "tw-components"; +import { parseError } from "utils/errorParser"; interface ClaimTabProps { contract: ThirdwebContract; @@ -67,7 +68,12 @@ const ClaimTabERC1155: React.FC = ({ toast.promise(promise, { loading: "Claiming NFT", success: "NFT claimed successfully", - error: "Failed to claim NFT", + error: (error) => { + return { + message: "Failed to claim NFT", + description: parseError(error), + }; + }, }); trackEvent({ category: "nft", diff --git a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/nfts/[tokenId]/components/update-metadata-form.tsx b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/nfts/[tokenId]/components/update-metadata-form.tsx index 1022a69b8ff..2c8ca2b5d3c 100644 --- a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/nfts/[tokenId]/components/update-metadata-form.tsx +++ b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/nfts/[tokenId]/components/update-metadata-form.tsx @@ -15,7 +15,6 @@ import { TransactionButton } from "components/buttons/TransactionButton"; import { PropertiesFormControl } from "components/contract-pages/forms/properties.shared"; import { FileInput } from "components/shared/FileInput"; import { useTrack } from "hooks/analytics/useTrack"; -import { useImageFileOrUrl } from "hooks/useImageFileOrUrl"; import { useTxNotifications } from "hooks/useTxNotifications"; import { type Dispatch, type SetStateAction, useMemo } from "react"; import { useForm } from "react-hook-form"; @@ -38,9 +37,12 @@ import { FormLabel, Heading, } from "tw-components"; -import { NFTMediaWithEmptyState } from "tw-components/nft-media"; import type { NFTMetadataInputLimited } from "types/modified-types"; import { parseAttributes } from "utils/parseAttributes"; +import { + getUploadedNFTMediaMeta, + handleNFTMediaUpload, +} from "../../../modules/components/nft/handleNFTMediaUpload"; const UPDATE_METADATA_FORM_ID = "nft-update-metadata-form"; @@ -83,6 +85,11 @@ export const UpdateNftMetadata: React.FC = ({ return nftMetadata; }, [nft]); + const form = useForm({ + defaultValues: transformedQueryData, + values: transformedQueryData, + }); + const { setValue, control, @@ -90,78 +97,20 @@ export const UpdateNftMetadata: React.FC = ({ watch, handleSubmit, formState: { errors, isDirty }, - } = useForm({ - defaultValues: transformedQueryData, - values: transformedQueryData, - }); + } = form; const setFile = (file: File) => { - if (file.type.includes("image")) { - // image files - setValue("image", file); - if (watch("external_url") instanceof File) { - setValue("external_url", undefined); - } - if (watch("animation_url") instanceof File) { - setValue("animation_url", undefined); - } - } else if ( - ["audio", "video", "text/html", "model/*"].some((type: string) => - file.type.includes(type), - ) || - file.name?.endsWith(".glb") || - file.name?.endsWith(".usdz") || - file.name?.endsWith(".gltf") || - file.name.endsWith(".obj") - ) { - // audio, video, html, and glb (3d) files - setValue("animation_url", file); - if (watch("external_url") instanceof File) { - setValue("external_url", undefined); - } - } else if ( - ["text", "application/pdf"].some((type: string) => - file.type?.includes(type), - ) - ) { - // text and pdf files - setValue("external_url", file); - if (watch("animation_url") instanceof File) { - setValue("animation_url", undefined); - } - } + handleNFTMediaUpload({ file, form }); }; + const { + media, + image, + mediaFileError, + showCoverImageUpload, + animation_url, + external_url, + } = getUploadedNFTMediaMeta(form); - const imageUrl = useImageFileOrUrl(watch("image") as File | string); - const animationUrlFormValue = watch("animation_url"); - const imageUrlFormValue = watch("image"); - - const mediaFileUrl = - watch("animation_url") instanceof File - ? watch("animation_url") - : watch("external_url") instanceof File - ? watch("external_url") - : watch("image") instanceof File - ? imageUrl - : undefined; - - const mediaFileError = - watch("animation_url") instanceof File - ? errors?.animation_url - : watch("external_url") instanceof File - ? errors?.external_url - : watch("image") instanceof File - ? errors?.image - : undefined; - - const externalUrl = watch("external_url"); - const externalIsTextFile = - externalUrl instanceof File && - (externalUrl.type.includes("text") || externalUrl.type.includes("pdf")); - - const showCoverImageUpload = - watch("animation_url") instanceof File || - watch("external_url") instanceof File; const sendAndConfirmTx = useSendAndConfirmTransaction(); const updateMetadataNotifications = useTxNotifications( "NFT metadata updated successfully", @@ -247,37 +196,20 @@ export const UpdateNftMetadata: React.FC = ({ {errors?.name?.message} + Media
- {nft?.metadata && !mediaFileUrl && ( - - )} -
@@ -289,13 +221,15 @@ export const UpdateNftMetadata: React.FC = ({ {mediaFileError?.message as unknown as string}
+ {showCoverImageUpload && ( Cover Image setValue("image", file)} className="rounded border border-border transition-all" @@ -320,6 +254,7 @@ export const UpdateNftMetadata: React.FC = ({ control={control} register={register} setValue={setValue} + client={contract.client} /> = ({ {errors?.background_color?.message} - {!externalIsTextFile && ( + + {!(external_url instanceof File) && ( External URL @@ -360,42 +296,43 @@ export const UpdateNftMetadata: React.FC = ({ )} - - Image URL - { - setValue("image", e.target.value); - }} - /> - - If you already have your NFT image pre-uploaded to a URL, you - can specify it here instead of uploading the media file - - {errors?.image?.message} - - - Animation URL - { - setValue("animation_url", e.target.value); - }} - /> - - If you already have your NFT Animation URL pre-uploaded to a - URL, you can specify it here instead of uploading the media file - - - {errors?.animation_url?.message} - - + + {!(image instanceof File) && ( + + Image URL + { + setValue("image", e.target.value); + }} + /> + + If you already have your NFT image pre-uploaded to a URL, you + can specify it here instead of uploading the media file + + {errors?.image?.message} + + )} + + {!(animation_url instanceof File) && ( + + Animation URL + { + setValue("animation_url", e.target.value); + }} + /> + + If you already have your NFT Animation URL pre-uploaded to a + URL, you can specify it here instead of uploading the media + file + + + {errors?.animation_url?.message} + + + )} @@ -415,7 +352,7 @@ export const UpdateNftMetadata: React.FC = ({ isPending={sendAndConfirmTx.isPending} form={UPDATE_METADATA_FORM_ID} type="submit" - disabled={!isDirty && imageUrl === nft?.metadata.image} + disabled={!isDirty} > Update NFT diff --git a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/nfts/components/lazy-mint-form.tsx b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/nfts/components/lazy-mint-form.tsx index abb33dbd286..851efebf5e3 100644 --- a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/nfts/components/lazy-mint-form.tsx +++ b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/nfts/components/lazy-mint-form.tsx @@ -17,7 +17,6 @@ import { TransactionButton } from "components/buttons/TransactionButton"; import { PropertiesFormControl } from "components/contract-pages/forms/properties.shared"; import { FileInput } from "components/shared/FileInput"; import { useTrack } from "hooks/analytics/useTrack"; -import { useImageFileOrUrl } from "hooks/useImageFileOrUrl"; import { useTxNotifications } from "hooks/useTxNotifications"; import type { Dispatch, SetStateAction } from "react"; import { useForm } from "react-hook-form"; @@ -34,6 +33,10 @@ import { } from "tw-components"; import type { NFTMetadataInputLimited } from "types/modified-types"; import { parseAttributes } from "utils/parseAttributes"; +import { + getUploadedNFTMediaMeta, + handleNFTMediaUpload, +} from "../../modules/components/nft/handleNFTMediaUpload"; const LAZY_MINT_FORM_ID = "nft-lazy-mint-form"; @@ -54,6 +57,8 @@ export const LazyMintNftForm: React.FC = ({ const address = useActiveAccount()?.address; const sendAndConfirmTx = useSendAndConfirmTransaction(); + const form = useForm(); + const { setValue, control, @@ -61,75 +66,20 @@ export const LazyMintNftForm: React.FC = ({ watch, handleSubmit, formState: { errors, isDirty }, - } = useForm(); + } = form; const setFile = (file: File) => { - if (file.type.includes("image")) { - // image files - setValue("image", file); - if (watch("external_url") instanceof File) { - setValue("external_url", undefined); - } - if (watch("animation_url") instanceof File) { - setValue("animation_url", undefined); - } - } else if ( - ["audio", "video", "text/html", "model/*"].some((type: string) => - file.type.includes(type), - ) || - file.name?.endsWith(".glb") || - file.name?.endsWith(".usdz") || - file.name?.endsWith(".gltf") || - file.name.endsWith(".obj") - ) { - // audio, video, html, and glb (3d) files - setValue("animation_url", file); - if (watch("external_url") instanceof File) { - setValue("external_url", undefined); - } - } else if ( - ["text", "application/pdf"].some((type: string) => - file.type?.includes(type), - ) - ) { - // text and pdf files - setValue("external_url", file); - if (watch("animation_url") instanceof File) { - setValue("animation_url", undefined); - } - } + handleNFTMediaUpload({ file, form }); }; - const imageUrl = useImageFileOrUrl(watch("image") as File | string); - const animationUrlFormValue = watch("animation_url"); - const imageUrlFormValue = watch("image"); - - const mediaFileUrl = - watch("animation_url") instanceof File - ? watch("animation_url") - : watch("external_url") instanceof File - ? watch("external_url") - : watch("image") instanceof File - ? imageUrl - : undefined; - - const mediaFileError = - watch("animation_url") instanceof File - ? errors?.animation_url - : watch("external_url") instanceof File - ? errors?.external_url - : watch("image") instanceof File - ? errors?.image - : undefined; - - const externalUrl = watch("external_url"); - const externalIsTextFile = - externalUrl instanceof File && - (externalUrl.type.includes("text") || externalUrl.type.includes("pdf")); - - const showCoverImageUpload = - watch("animation_url") instanceof File || - watch("external_url") instanceof File; + const { + media, + image, + mediaFileError, + showCoverImageUpload, + animation_url, + external_url, + } = getUploadedNFTMediaMeta(form); const lazyMintNotifications = useTxNotifications( "NFT lazy minted successfully", @@ -184,7 +134,7 @@ export const LazyMintNftForm: React.FC = ({ })} >
- Metadata + Metadata
@@ -197,13 +147,14 @@ export const LazyMintNftForm: React.FC = ({
@@ -219,8 +170,9 @@ export const LazyMintNftForm: React.FC = ({ Cover Image setValue("image", file)} className="rounded border border-border transition-all" @@ -245,6 +197,7 @@ export const LazyMintNftForm: React.FC = ({ control={control} register={register} setValue={setValue} + client={contract.client} /> = ({ {errors?.background_color?.message}
- {!externalIsTextFile && ( + + {!(external_url instanceof File) && ( External URL @@ -286,47 +240,45 @@ export const LazyMintNftForm: React.FC = ({ )} - - Image URL - { - setValue("image", e.target.value); - }} - /> - - If you already have your NFT image pre-uploaded to a URL, you - can specify it here instead of uploading the media file - - {errors?.image?.message} - - - Animation URL - { - setValue("animation_url", e.target.value); - }} - /> - - If you already have your NFT Animation URL pre-uploaded to a - URL, you can specify it here instead of uploading the media - file - - - {errors?.animation_url?.message} - - + + {!(image instanceof File) && ( + + Image URL + { + setValue("image", e.target.value); + }} + /> + + If you already have your NFT image pre-uploaded to a URL, + you can specify it here instead of uploading the media file + + {errors?.image?.message} + + )} + + {!(animation_url instanceof File) && ( + + Animation URL + { + setValue("animation_url", e.target.value); + }} + /> + + If you already have your NFT Animation URL pre-uploaded to a + URL, you can specify it here instead of uploading the media + file + + + {errors?.animation_url?.message} + + + )} diff --git a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/nfts/components/mint-form.tsx b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/nfts/components/mint-form.tsx index 37026578bf9..67fce1df341 100644 --- a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/nfts/components/mint-form.tsx +++ b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/nfts/components/mint-form.tsx @@ -16,7 +16,6 @@ import { TransactionButton } from "components/buttons/TransactionButton"; import { PropertiesFormControl } from "components/contract-pages/forms/properties.shared"; import { FileInput } from "components/shared/FileInput"; import { useTrack } from "hooks/analytics/useTrack"; -import { useImageFileOrUrl } from "hooks/useImageFileOrUrl"; import { useTxNotifications } from "hooks/useTxNotifications"; import type { Dispatch, SetStateAction } from "react"; import { useForm } from "react-hook-form"; @@ -34,6 +33,10 @@ import { } from "tw-components"; import type { NFTMetadataInputLimited } from "types/modified-types"; import { parseAttributes } from "utils/parseAttributes"; +import { + getUploadedNFTMediaMeta, + handleNFTMediaUpload, +} from "../../modules/components/nft/handleNFTMediaUpload"; const MINT_FORM_ID = "nft-mint-form"; @@ -52,6 +55,12 @@ export const NFTMintForm: React.FC = ({ }) => { const trackEvent = useTrack(); const address = useActiveAccount()?.address; + const form = useForm< + NFTMetadataInputLimited & { + supply: number; + } + >(); + const { setValue, control, @@ -59,78 +68,20 @@ export const NFTMintForm: React.FC = ({ watch, handleSubmit, formState: { errors, isDirty }, - } = useForm< - NFTMetadataInputLimited & { - supply: number; - } - >(); + } = form; const setFile = (file: File) => { - if (file.type.includes("image")) { - // image files - setValue("image", file); - if (watch("external_url") instanceof File) { - setValue("external_url", undefined); - } - if (watch("animation_url") instanceof File) { - setValue("animation_url", undefined); - } - } else if ( - ["audio", "video", "text/html", "model/*"].some((type: string) => - file.type.includes(type), - ) || - file.name?.endsWith(".glb") || - file.name?.endsWith(".usdz") || - file.name?.endsWith(".gltf") || - file.name.endsWith(".obj") - ) { - // audio, video, html, and glb (3d) files - setValue("animation_url", file); - if (watch("external_url") instanceof File) { - setValue("external_url", undefined); - } - } else if ( - ["text", "application/pdf"].some((type: string) => - file.type?.includes(type), - ) - ) { - // text and pdf files - setValue("external_url", file); - if (watch("animation_url") instanceof File) { - setValue("animation_url", undefined); - } - } + handleNFTMediaUpload({ file, form }); }; - const imageUrl = useImageFileOrUrl(watch("image") as File | string); - const animationUrlFormValue = watch("animation_url"); - const imageUrlFormValue = watch("image"); - const mediaFileUrl = - watch("animation_url") instanceof File - ? watch("animation_url") - : watch("external_url") instanceof File - ? watch("external_url") - : watch("image") instanceof File - ? imageUrl - : undefined; - - const mediaFileError = - watch("animation_url") instanceof File - ? errors?.animation_url - : watch("external_url") instanceof File - ? errors?.external_url - : watch("image") instanceof File - ? errors?.image - : undefined; - - const externalUrl = watch("external_url"); - const externalIsTextFile = - externalUrl instanceof File && - (externalUrl.type.includes("text") || externalUrl.type.includes("pdf")); - - const showCoverImageUpload = - watch("animation_url") instanceof File || - watch("external_url") instanceof File; + const { + media, + image, + mediaFileError, + showCoverImageUpload, + animation_url, + external_url, + } = getUploadedNFTMediaMeta(form); const sendAndConfirmTx = useSendAndConfirmTransaction(); const nftMintNotifications = useTxNotifications( @@ -198,7 +149,7 @@ export const NFTMintForm: React.FC = ({ })} >
- Metadata + Metadata xx
@@ -211,7 +162,8 @@ export const NFTMintForm: React.FC = ({
= ({ Cover Image setValue("image", file)} className="shrink-0 rounded border border-border transition-all" @@ -266,6 +219,7 @@ export const NFTMintForm: React.FC = ({ )} = ({ Advanced Options - + Background Color @@ -296,7 +250,8 @@ export const NFTMintForm: React.FC = ({ {errors?.background_color?.message} - {!externalIsTextFile && ( + + {!(external_url instanceof File) && ( External URL @@ -312,44 +267,42 @@ export const NFTMintForm: React.FC = ({ )} - - Image URL - { - setValue("image", e.target.value); - }} - /> - - If you already have your NFT image pre-uploaded to a URL, you - can specify it here instead of uploading the asset - - {errors?.image?.message} - - - Animation URL - { - setValue("animation_url", e.target.value); - }} - /> - - If you already have your NFT Animation URL pre-uploaded to a - URL, you can specify it here instead of uploading the asset - - - {errors?.animation_url?.message} - - + + {!(image instanceof File) && ( + + Image URL + { + setValue("image", e.target.value); + }} + /> + + If you already have your NFT image pre-uploaded to a URL, + you can specify it here instead of uploading the asset + + {errors?.image?.message} + + )} + + {!(animation_url instanceof File) && ( + + Animation URL + { + setValue("animation_url", e.target.value); + }} + /> + + If you already have your NFT Animation URL pre-uploaded to a + URL, you can specify it here instead of uploading the asset + + + {errors?.animation_url?.message} + + + )} diff --git a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/nfts/components/shared-metadata-form.tsx b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/nfts/components/shared-metadata-form.tsx index afa81a8359b..88cc8f4e313 100644 --- a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/nfts/components/shared-metadata-form.tsx +++ b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/nfts/components/shared-metadata-form.tsx @@ -14,7 +14,6 @@ import { import { TransactionButton } from "components/buttons/TransactionButton"; import { FileInput } from "components/shared/FileInput"; import { useTrack } from "hooks/analytics/useTrack"; -import { useImageFileOrUrl } from "hooks/useImageFileOrUrl"; import { useTxNotifications } from "hooks/useTxNotifications"; import type { Dispatch, SetStateAction } from "react"; import { useForm } from "react-hook-form"; @@ -31,6 +30,10 @@ import { } from "tw-components"; import type { NFTMetadataInputLimited } from "types/modified-types"; import { parseAttributes } from "utils/parseAttributes"; +import { + getUploadedNFTMediaMeta, + handleNFTMediaUpload, +} from "../../modules/components/nft/handleNFTMediaUpload"; const SHARED_METADATA_FORM_ID = "shared-metadata-form"; @@ -42,75 +45,20 @@ export const SharedMetadataForm: React.FC<{ const trackEvent = useTrack(); const address = useActiveAccount()?.address; const sendAndConfirmTx = useSendAndConfirmTransaction(); + const form = useForm(); const { setValue, register, - watch, handleSubmit, formState: { errors, isDirty }, - } = useForm(); + } = form; const setFile = (file: File) => { - if (file.type.includes("image")) { - // image files - setValue("image", file); - if (watch("external_url") instanceof File) { - setValue("external_url", undefined); - } - if (watch("animation_url") instanceof File) { - setValue("animation_url", undefined); - } - } else if ( - ["audio", "video", "text/html", "model/*"].some((type: string) => - file.type.includes(type), - ) || - file.name?.endsWith(".glb") || - file.name?.endsWith(".usdz") || - file.name?.endsWith(".gltf") || - file.name.endsWith(".obj") - ) { - // audio, video, html, and glb (3d) files - setValue("animation_url", file); - if (watch("external_url") instanceof File) { - setValue("external_url", undefined); - } - } else if ( - ["text", "application/pdf"].some((type: string) => - file.type?.includes(type), - ) - ) { - // text and pdf files - setValue("external_url", file); - if (watch("animation_url") instanceof File) { - setValue("animation_url", undefined); - } - } + handleNFTMediaUpload({ file, form }); }; - const imageUrl = useImageFileOrUrl(watch("image") as File | string); - const animationUrlFormValue = watch("animation_url"); - const imageUrlFormValue = watch("image"); - const mediaFileUrl = - watch("animation_url") instanceof File - ? watch("animation_url") - : watch("external_url") instanceof File - ? watch("external_url") - : watch("image") instanceof File - ? imageUrl - : undefined; - - const mediaFileError = - watch("animation_url") instanceof File - ? errors?.animation_url - : watch("external_url") instanceof File - ? errors?.external_url - : watch("image") instanceof File - ? errors?.image - : undefined; - - const showCoverImageUpload = - watch("animation_url") instanceof File || - watch("external_url") instanceof File; + const { media, image, mediaFileError, showCoverImageUpload, animation_url } = + getUploadedNFTMediaMeta(form); const setSharedMetaNotifications = useTxNotifications( "Shared metadata updated successfully", @@ -185,7 +133,8 @@ export const SharedMetadataForm: React.FC<{
Cover Image setValue("image", file)} className="shrink-0 rounded border border-border transition-all" @@ -237,45 +187,42 @@ export const SharedMetadataForm: React.FC<{ Advanced Options - - - Image URL - { - setValue("image", e.target.value); - }} - /> - - If you already have your NFT image pre-uploaded, you can set - the URL or URI here. - - {errors?.image?.message} - - - Animation URL - { - setValue("animation_url", e.target.value); - }} - /> - - If you already have your NFT Animation URL pre-uploaded, you - can set the URL or URI here. - - - {errors?.animation_url?.message} - - + + {!(image instanceof File) && ( + + Image URL + { + setValue("image", e.target.value); + }} + /> + + If you already have your NFT image pre-uploaded, you can set + the URL or URI here. + + {errors?.image?.message} + + )} + + {!(animation_url instanceof File) && ( + + Animation URL + { + setValue("animation_url", e.target.value); + }} + /> + + If you already have your NFT Animation URL pre-uploaded, you + can set the URL or URI here. + + + {errors?.animation_url?.message} + + + )} diff --git a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/nfts/components/table.tsx b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/nfts/components/table.tsx index ca42ff04ea3..34e2ed09b68 100644 --- a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/nfts/components/table.tsx +++ b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/nfts/components/table.tsx @@ -139,7 +139,7 @@ export const NFTGetAllTable: React.FC = ({ } if (isErc1155) { cols.push({ - Header: "Supply", + Header: "Circulating Supply", accessor: (row) => row, Cell: (cell: CellProps) => { if (cell.row.original.type === "ERC1155") { diff --git a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/settings/components/metadata.tsx b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/settings/components/metadata.tsx index 7dc46eacd0c..eb452f6a9d8 100644 --- a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/settings/components/metadata.tsx +++ b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/settings/components/metadata.tsx @@ -14,7 +14,6 @@ import { TransactionButton } from "components/buttons/TransactionButton"; import { FileInput } from "components/shared/FileInput"; import { CommonContractSchema } from "constants/schemas"; import { useTrack } from "hooks/analytics/useTrack"; -import { useImageFileOrUrl } from "hooks/useImageFileOrUrl"; import { useTxNotifications } from "hooks/useTxNotifications"; import { PlusIcon, Trash2Icon } from "lucide-react"; import { useMemo } from "react"; @@ -224,9 +223,10 @@ export const SettingsMetadata = ({ > Image setValue("image", file, { shouldTouch: true, diff --git a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/tokens/components/airdrop-upload.tsx b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/tokens/components/airdrop-upload.tsx index bb292611fbc..7b715894668 100644 --- a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/tokens/components/airdrop-upload.tsx +++ b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/tokens/components/airdrop-upload.tsx @@ -6,6 +6,7 @@ import { Link } from "@chakra-ui/react"; import { useCsvUpload } from "hooks/useCsvUpload"; import { CircleAlertIcon, UploadIcon } from "lucide-react"; import { useMemo, useRef } from "react"; +import { useDropzone } from "react-dropzone"; import type { Column } from "react-table"; import { type ThirdwebClient, ZERO_ADDRESS } from "thirdweb"; import { Button, Heading, Text } from "tw-components"; @@ -36,19 +37,14 @@ export const AirdropUpload: React.FC = ({ onClose, client, }) => { - const { - normalizeQuery, - getInputProps, - getRootProps, - isDragActive, - rawData, - noCsv, - reset, - removeInvalid, - } = useCsvUpload({ csvParser, client }); + const csvUpload = useCsvUpload({ csvParser, client }); + const dropzone = useDropzone({ + onDrop: csvUpload.setFiles, + }); + const paginationPortalRef = useRef(null); - const normalizeData = normalizeQuery.data; + const normalizeData = csvUpload.normalizeQuery.data; const columns = useMemo(() => { return [ @@ -107,11 +103,11 @@ export const AirdropUpload: React.FC = ({ return (
- {normalizeData.result.length && rawData.length > 0 ? ( + {normalizeData.result.length && csvUpload.rawData.length > 0 ? ( <> portalRef={paginationPortalRef} - data={normalizeQuery.data.result} + data={csvUpload.normalizeQuery.data.result} columns={columns} />
@@ -119,21 +115,21 @@ export const AirdropUpload: React.FC = ({
- {normalizeQuery.data.invalidFound ? ( + {csvUpload.normalizeQuery.data.invalidFound ? ( @@ -159,18 +155,18 @@ export const AirdropUpload: React.FC = ({
- +
- {isDragActive ? ( + {dropzone.isDragActive ? ( Drop the files here @@ -178,9 +174,9 @@ export const AirdropUpload: React.FC = ({ - {noCsv + {csvUpload.noCsv ? `No valid CSV file found, make sure your CSV includes the "address" & "quantity" column.` : "Drag & Drop a CSV file here"} diff --git a/apps/dashboard/src/app/(app)/account/settings/AccountSettingsPageUI.tsx b/apps/dashboard/src/app/(app)/account/settings/AccountSettingsPageUI.tsx index 1b1056880e9..4f2e759f85a 100644 --- a/apps/dashboard/src/app/(app)/account/settings/AccountSettingsPageUI.tsx +++ b/apps/dashboard/src/app/(app)/account/settings/AccountSettingsPageUI.tsx @@ -137,12 +137,12 @@ function AccountAvatarFormControl(props: {

diff --git a/apps/dashboard/src/app/(app)/login/onboarding/team-onboarding/TeamInfoForm.stories.tsx b/apps/dashboard/src/app/(app)/login/onboarding/team-onboarding/TeamInfoForm.stories.tsx index a8508452e42..4998ad6eab3 100644 --- a/apps/dashboard/src/app/(app)/login/onboarding/team-onboarding/TeamInfoForm.stories.tsx +++ b/apps/dashboard/src/app/(app)/login/onboarding/team-onboarding/TeamInfoForm.stories.tsx @@ -1,6 +1,6 @@ import type { Meta, StoryObj } from "@storybook/react"; import { teamStub } from "stories/stubs"; -import { storybookLog } from "stories/utils"; +import { storybookLog, storybookThirdwebClient } from "stories/utils"; import { TeamOnboardingLayout } from "../onboarding-layout"; import { TeamInfoFormUI } from "./TeamInfoForm"; @@ -35,6 +35,7 @@ function Story(props: { return ( { storybookLog("onComplete"); }} diff --git a/apps/dashboard/src/app/(app)/login/onboarding/team-onboarding/TeamInfoForm.tsx b/apps/dashboard/src/app/(app)/login/onboarding/team-onboarding/TeamInfoForm.tsx index f0f08859feb..c5fa09e6093 100644 --- a/apps/dashboard/src/app/(app)/login/onboarding/team-onboarding/TeamInfoForm.tsx +++ b/apps/dashboard/src/app/(app)/login/onboarding/team-onboarding/TeamInfoForm.tsx @@ -18,6 +18,7 @@ import { ArrowRightIcon } from "lucide-react"; import { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; +import type { ThirdwebClient } from "thirdweb"; import { useDebounce } from "use-debounce"; import { z } from "zod"; import { @@ -46,6 +47,7 @@ export function TeamInfoFormUI(props: { onComplete: (updatedTeam: Team) => void; isTeamSlugAvailable: (slug: string) => Promise; teamSlug: string; + client: ThirdwebClient; }) { const form = useForm({ resolver: zodResolver(formSchema), @@ -159,6 +161,7 @@ export function TeamInfoFormUI(props: {
onChange(file)} diff --git a/apps/dashboard/src/app/(app)/login/onboarding/team-onboarding/team-onboarding.tsx b/apps/dashboard/src/app/(app)/login/onboarding/team-onboarding/team-onboarding.tsx index ada5027196e..54bb9b18093 100644 --- a/apps/dashboard/src/app/(app)/login/onboarding/team-onboarding/team-onboarding.tsx +++ b/apps/dashboard/src/app/(app)/login/onboarding/team-onboarding/team-onboarding.tsx @@ -38,6 +38,7 @@ export function TeamInfoForm(props: { return res.data.result; }} teamSlug={props.teamSlug} + client={props.client} onComplete={(updatedTeam) => { router.replace(`/get-started/team/${updatedTeam.slug}/add-members`); }} diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/components/client/auth-options-form.client.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/components/client/auth-options-form.client.tsx index 82ca6113ee0..301a2680917 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/components/client/auth-options-form.client.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/configuration/components/client/auth-options-form.client.tsx @@ -230,7 +230,7 @@ export function AuthOptionsForm({
{ - console.log(errors); + console.error(errors); })} className="flex flex-col gap-8" > diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/settings/general/TeamGeneralSettingsPageUI.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/settings/general/TeamGeneralSettingsPageUI.tsx index 09c8dc6b90b..198dba545d5 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/settings/general/TeamGeneralSettingsPageUI.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/settings/general/TeamGeneralSettingsPageUI.tsx @@ -221,11 +221,11 @@ function TeamAvatarFormControl(props: {
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/assets/cards.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/assets/cards.tsx index c9ba6684621..d1c93a30421 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/assets/cards.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/assets/cards.tsx @@ -1,6 +1,5 @@ "use client"; -import { Badge } from "@/components/ui/badge"; import { cn } from "@/lib/utils"; import { ImportModal } from "components/contract-components/import-contract/modal"; import { useTrack } from "hooks/analytics/useTrack"; @@ -34,18 +33,17 @@ export function Cards(props: { void; icon: React.FC<{ className?: string }>; trackingLabel: string; - badge?: string; }) { const { onClick } = props; const isClickable = !!onClick || !!props.href; @@ -111,17 +108,6 @@ function CardLink(props: {
- {props.badge && ( -
- - {props.badge} - -
- )} -

{props.href ? ( diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/assets/create/_common/PageHeader.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/assets/create/_common/PageHeader.tsx new file mode 100644 index 00000000000..4bd5121591f --- /dev/null +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/assets/create/_common/PageHeader.tsx @@ -0,0 +1,56 @@ +import { + Breadcrumb, + BreadcrumbItem, + BreadcrumbLink, + BreadcrumbList, + BreadcrumbPage, + BreadcrumbSeparator, +} from "@/components/ui/breadcrumb"; +import { cn } from "@/lib/utils"; +import Link from "next/link"; + +export function CreateAssetPageHeader(props: { + teamSlug: string; + projectSlug: string; + title: string; + description: string; + containerClassName: string; +}) { + return ( +
+
+ + + + + + Assets + + + + + + {props.title} + + + +
+ +
+
+

+ {props.title} +

+

{props.description}

+
+
+
+ ); +} diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/assets/create/_common/SocialUrls.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/assets/create/_common/SocialUrls.tsx new file mode 100644 index 00000000000..0b3fcab5579 --- /dev/null +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/assets/create/_common/SocialUrls.tsx @@ -0,0 +1,106 @@ +"use client"; + +import { Button } from "@/components/ui/button"; +import { + FormControl, + FormField, + FormItem, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { PlusIcon, Trash2Icon } from "lucide-react"; +import { type UseFormReturn, useFieldArray } from "react-hook-form"; + +type WithSocialUrls = { + socialUrls: { + url: string; + platform: string; + }[]; +}; + +export function SocialUrlsFieldset(props: { + form: UseFormReturn; +}) { + // T contains all properties of WithSocialUrls, so this is ok + const form = props.form as unknown as UseFormReturn; + + const { fields, append, remove } = useFieldArray({ + name: "socialUrls", + control: form.control, + }); + + return ( +
+

Social URLs

+ + {fields.length > 0 && ( +
+ {fields.map((field, index) => ( +
+
+ ( + + + + + + + )} + /> + + ( + + + + + + + )} + /> +
+ + +
+ ))} +
+ )} + + +
+ ); +} diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/assets/create/_common/chain-overview.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/assets/create/_common/chain-overview.tsx new file mode 100644 index 00000000000..9686de9a5a9 --- /dev/null +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/assets/create/_common/chain-overview.tsx @@ -0,0 +1,24 @@ +import { ChainIconClient } from "components/icons/ChainIcon"; +import { useAllChainsData } from "hooks/chains/allChains"; +import type { ThirdwebClient } from "thirdweb/dist/types/client/client"; + +export function ChainOverview(props: { + chainId: string; + client: ThirdwebClient; +}) { + const { idToChain } = useAllChainsData(); + const chainMetadata = idToChain.get(Number(props.chainId)); + + return ( +
+ +

+ {chainMetadata?.name || `Chain ${props.chainId}`} +

+
+ ); +} diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/assets/create/_common/download-file-button.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/assets/create/_common/download-file-button.tsx new file mode 100644 index 00000000000..5c7b765e0cd --- /dev/null +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/assets/create/_common/download-file-button.tsx @@ -0,0 +1,42 @@ +import { Button } from "@/components/ui/button"; +import { ArrowDownToLineIcon } from "lucide-react"; + +export function handleDownload(params: { + fileContent: string; + fileNameWithExtension: string; + fileFormat: "text/csv" | "application/json"; +}) { + const link = document.createElement("a"); + const blob = new Blob([params.fileContent], { + type: params.fileFormat, + }); + const objectURL = URL.createObjectURL(blob); + link.href = objectURL; + link.download = params.fileNameWithExtension; + link.click(); + URL.revokeObjectURL(objectURL); +} + +export function DownloadFileButton(props: { + fileContent: string; + fileNameWithExtension: string; + fileFormat: "text/csv" | "application/json"; + label: string; +}) { + return ( + + ); +} diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/assets/create/_common/file-preview.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/assets/create/_common/file-preview.tsx new file mode 100644 index 00000000000..2e001efef17 --- /dev/null +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/assets/create/_common/file-preview.tsx @@ -0,0 +1,64 @@ +import { Img } from "@/components/blocks/Img"; +import { fileToBlobUrl } from "@/lib/file-to-url"; +import { cn } from "@/lib/utils"; +import { ImageOffIcon } from "lucide-react"; +import { useEffect, useState } from "react"; +import type { ThirdwebClient } from "thirdweb"; +import { MediaRenderer } from "thirdweb/react"; + +export function FilePreview(props: { + srcOrFile: File | string | undefined; + fallback?: React.ReactNode; + className?: string; + client: ThirdwebClient; + imgContainerClassName?: string; +}) { + const [objectUrl, setObjectUrl] = useState(""); + + // eslint-disable-next-line no-restricted-syntax + useEffect(() => { + if (props.srcOrFile instanceof File) { + const url = fileToBlobUrl(props.srcOrFile); + setObjectUrl(url); + return () => { + URL.revokeObjectURL(url); + }; + } else if (typeof props.srcOrFile === "string") { + setObjectUrl(props.srcOrFile); + } else { + setObjectUrl(""); + } + }, [props.srcOrFile]); + + // shortcut just for images + const isImage = + props.srcOrFile instanceof File && + props.srcOrFile.type.startsWith("image/"); + + if (!objectUrl) { + return ( +
+ +
+ ); + } + + if (isImage) { + return ( + + ); + } + + return ( + div]:!bg-muted")} + /> + ); +} diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/assets/create/_common/schema.ts b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/assets/create/_common/schema.ts new file mode 100644 index 00000000000..c344c60dd44 --- /dev/null +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/assets/create/_common/schema.ts @@ -0,0 +1,35 @@ +import { isAddress } from "thirdweb"; +import * as z from "zod"; + +const urlSchema = z.string().url(); + +export const socialUrlsSchema = z.array( + z.object({ + platform: z.string(), + url: z.string().refine( + (val) => { + if (val === "") { + return true; + } + + const url = val.startsWith("http") ? val : `https://${val}`; + return urlSchema.safeParse(url).success; + }, + { + message: "Invalid URL", + }, + ), + }), +); + +export const addressSchema = z.string().refine( + (value) => { + if (isAddress(value)) { + return true; + } + return false; + }, + { + message: "Invalid address", + }, +); diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/assets/create/_common/step-card.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/assets/create/_common/step-card.tsx new file mode 100644 index 00000000000..ee1331d5ace --- /dev/null +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/assets/create/_common/step-card.tsx @@ -0,0 +1,98 @@ +import { Button } from "@/components/ui/button"; +import { useTrack } from "hooks/analytics/useTrack"; +import { ArrowLeftIcon, ArrowRightIcon } from "lucide-react"; +import { getStepCardTrackingData } from "../token/_common/tracking"; + +export function StepCard(props: { + title: string; + tracking: { + page: string; + contractType: "DropERC20" | "NFTCollection"; + }; + prevButton: + | undefined + | { + onClick: () => void; + }; + nextButton: + | undefined + | { + type: "submit"; + disabled?: boolean; + } + | { + type: "custom"; + custom: React.ReactNode; + } + | { + type: "click"; + disabled?: boolean; + onClick: () => void; + }; + children: React.ReactNode; +}) { + const trackEvent = useTrack(); + const nextButton = props.nextButton; + return ( +
+

+ {props.title} +

+ + {props.children} + + {(props.prevButton || props.nextButton) && ( +
+ {props.prevButton && ( + + )} + + {nextButton && nextButton.type !== "custom" && ( + + )} + + {props.nextButton && + props.nextButton.type === "custom" && + props.nextButton.custom} +
+ )} +
+ ); +} diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/assets/create/create-token-card.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/assets/create/create-token-card.tsx deleted file mode 100644 index f46166dce68..00000000000 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/assets/create/create-token-card.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import { Button } from "@/components/ui/button"; -import { useTrack } from "hooks/analytics/useTrack"; -import { ArrowLeftIcon, ArrowRightIcon } from "lucide-react"; -import { getStepCardTrackingData } from "./tracking"; - -export function StepCard(props: { - title: string; - page: "info" | "distribution" | "launch"; - prevButton: - | undefined - | { - onClick: () => void; - }; - nextButton: - | undefined - | { - type: "submit"; - disabled?: boolean; - } - | { - type: "custom"; - custom: React.ReactNode; - }; - children: React.ReactNode; -}) { - const trackEvent = useTrack(); - return ( -
-

- {props.title} -

- - {props.children} - -
- {props.prevButton && ( - - )} - - {props.nextButton && props.nextButton.type === "submit" && ( - - )} - - {props.nextButton && - props.nextButton.type === "custom" && - props.nextButton.custom} -
-
- ); -} diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/assets/create/nft/_common/form.ts b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/assets/create/nft/_common/form.ts new file mode 100644 index 00000000000..03cac30d6f4 --- /dev/null +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/assets/create/nft/_common/form.ts @@ -0,0 +1,64 @@ +import { isAddress } from "thirdweb"; +import * as z from "zod"; +import { socialUrlsSchema } from "../../_common/schema"; +import type { NFTMetadataWithPrice } from "../upload-nfts/batch-upload/process-files"; + +export const nftCollectionInfoFormSchema = z.object({ + name: z.string().min(1, "Name is required"), + symbol: z.string(), + chain: z.string().min(1, "Chain is required"), + description: z.string().optional(), + image: z.instanceof(File).optional(), + socialUrls: socialUrlsSchema, +}); + +const addressSchema = z.string().refine((value) => { + if (isAddress(value)) { + return true; + } + + return false; +}); + +export const nftSalesSettingsFormSchema = z.object({ + royaltyRecipient: addressSchema, + primarySaleRecipient: addressSchema, + royaltyBps: z.coerce.number().min(0).max(10000), +}); + +export type NFTCollectionInfoFormValues = z.infer< + typeof nftCollectionInfoFormSchema +>; + +export type CreateNFTCollectionAllValues = { + collectionInfo: NFTCollectionInfoFormValues; + nfts: NFTMetadataWithPrice[]; + sales: NFTSalesSettingsFormValues; +}; + +export type CreateNFTCollectionFunctions = { + erc721: { + deployContract: (values: CreateNFTCollectionAllValues) => Promise<{ + contractAddress: string; + }>; + setClaimConditions: (values: CreateNFTCollectionAllValues) => Promise; + lazyMintNFTs: (values: CreateNFTCollectionAllValues) => Promise; + }; + erc1155: { + deployContract: (values: CreateNFTCollectionAllValues) => Promise<{ + contractAddress: string; + }>; + setClaimConditions: (params: { + values: CreateNFTCollectionAllValues; + batch: { + startIndex: number; + count: number; + }; + }) => Promise; + lazyMintNFTs: (values: CreateNFTCollectionAllValues) => Promise; + }; +}; + +export type NFTSalesSettingsFormValues = z.infer< + typeof nftSalesSettingsFormSchema +>; diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/assets/create/nft/_common/pages.ts b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/assets/create/nft/_common/pages.ts new file mode 100644 index 00000000000..f86b3dec638 --- /dev/null +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/assets/create/nft/_common/pages.ts @@ -0,0 +1,6 @@ +export const nftCreationPages = { + "collection-info": "collection-info", + "upload-assets": "upload-assets", + "sales-settings": "sales-settings", + "launch-nft": "launch-nft", +} as const; diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/assets/create/nft/_common/tracking.ts b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/assets/create/nft/_common/tracking.ts new file mode 100644 index 00000000000..4628cb2ae47 --- /dev/null +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/assets/create/nft/_common/tracking.ts @@ -0,0 +1,91 @@ +// example: asset.claim-conditions.attempt +export function getNFTStepTrackingData( + params: { + action: "claim-conditions" | "lazy-mint" | "mint" | "deploy"; + ercType: "erc721" | "erc1155"; + chainId: number; + } & ( + | { + status: "attempt" | "success"; + } + | { + status: "error"; + errorMessage: string; + } + ), +) { + return { + category: "asset", + action: params.action, + contractType: params.ercType === "erc721" ? "DropERC721" : "DropERC1155", + label: params.status, + chainId: params.chainId, + ...(params.status === "error" + ? { + errorMessage: params.errorMessage, + } + : {}), + }; +} + +export function getNFTLaunchTrackingData( + params: { + chainId: number; + ercType: "erc721" | "erc1155"; + } & ( + | { + type: "attempt" | "success"; + } + | { + type: "error"; + errorMessage: string; + } + ), +) { + return { + category: "asset", + action: "launch", + label: params.type, + contractType: "NFTCollection", + ercType: params.ercType, + chainId: params.chainId, + ...(params.type === "error" + ? { + errorMessage: params.errorMessage, + } + : {}), + }; +} + +export function getNFTDeploymentTrackingData( + params: { + chainId: number; + ercType: "erc721" | "erc1155"; + } & ( + | { + type: "attempt" | "success"; + } + | { + type: "error"; + errorMessage: string; + } + ), +) { + // using "custom-contract" because it has to match the main deployment tracking format + return { + category: "custom-contract", + action: "deploy", + label: params.type, + publisherAndContractName: + params.ercType === "erc721" + ? "deployer.thirdweb.eth/DropERC721" + : "deployer.thirdweb.eth/DropERC1155", + chainId: params.chainId, + deploymentType: "asset", + ...(params.type === "error" + ? { + errorMessage: params.errorMessage, + } + : {}), + }; +} diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/assets/create/nft/collection-info/nft-collection-info-fieldset.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/assets/create/nft/collection-info/nft-collection-info-fieldset.tsx new file mode 100644 index 00000000000..99af8ef0047 --- /dev/null +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/assets/create/nft/collection-info/nft-collection-info-fieldset.tsx @@ -0,0 +1,134 @@ +"use client"; + +import { FormFieldSetup } from "@/components/blocks/FormFieldSetup"; +import { SingleNetworkSelector } from "@/components/blocks/NetworkSelectors"; +import { Form } from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { Textarea } from "@/components/ui/textarea"; +import { ClientOnly } from "components/ClientOnly/ClientOnly"; +import { FileInput } from "components/shared/FileInput"; +import type { UseFormReturn } from "react-hook-form"; +import type { ThirdwebClient } from "thirdweb"; +import { SocialUrlsFieldset } from "../../_common/SocialUrls"; +import { StepCard } from "../../_common/step-card"; +import type { NFTCollectionInfoFormValues } from "../_common/form"; +import { nftCreationPages } from "../_common/pages"; + +export function NFTCollectionInfoFieldset(props: { + client: ThirdwebClient; + onNext: () => void; + form: UseFormReturn; + onChainUpdated: () => void; +}) { + const { form } = props; + return ( + + + +
+ {/* left */} + + + form.setValue("image", file, { + shouldTouch: true, + }) + } + className="rounded-lg border-border bg-background transition-all duration-200 hover:border-active-border hover:bg-background" + /> + + + {/* right */} +
+ {/* name + symbol */} +
+ + + + + + + +
+ + {/* chain */} + + + { + form.setValue("chain", chain.toString()); + props.onChainUpdated(); + }} + disableChainId + /> + + + + +