Skip to content

Commit e21b6c3

Browse files
committed
implement MetadataUpload
1 parent 543b68f commit e21b6c3

File tree

8 files changed

+2491
-1808
lines changed

8 files changed

+2491
-1808
lines changed

apps/dashboard/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"private": true,
55
"scripts": {
66
"preinstall": "npx only-allow pnpm",
7-
"dev": "next dev --turbo",
7+
"dev": "next dev",
88
"build": "next build",
99
"start": "next start",
1010
"format": "biome format ./src --write",
@@ -30,6 +30,7 @@
3030
"@hookform/resolvers": "^3.9.0",
3131
"@marsidev/react-turnstile": "^1.0.2",
3232
"@n8tb1t/use-scroll-position": "^2.0.3",
33+
"@radix-ui/react-accordion": "^1.2.1",
3334
"@radix-ui/react-alert-dialog": "^1.1.2",
3435
"@radix-ui/react-avatar": "^1.1.1",
3536
"@radix-ui/react-checkbox": "^1.1.2",
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
"use client";
2+
3+
import * as AccordionPrimitive from "@radix-ui/react-accordion";
4+
import { ChevronDown } from "lucide-react";
5+
import * as React from "react";
6+
7+
import { cn } from "@/lib/utils";
8+
9+
const Accordion = AccordionPrimitive.Root;
10+
11+
const AccordionItem = React.forwardRef<
12+
React.ElementRef<typeof AccordionPrimitive.Item>,
13+
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
14+
>(({ className, ...props }, ref) => (
15+
<AccordionPrimitive.Item
16+
ref={ref}
17+
className={cn("border-b", className)}
18+
{...props}
19+
/>
20+
));
21+
AccordionItem.displayName = "AccordionItem";
22+
23+
const AccordionTrigger = React.forwardRef<
24+
React.ElementRef<typeof AccordionPrimitive.Trigger>,
25+
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
26+
>(({ className, children, ...props }, ref) => (
27+
<AccordionPrimitive.Header className="flex">
28+
<AccordionPrimitive.Trigger
29+
ref={ref}
30+
className={cn(
31+
"flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
32+
className,
33+
)}
34+
{...props}
35+
>
36+
{children}
37+
<ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
38+
</AccordionPrimitive.Trigger>
39+
</AccordionPrimitive.Header>
40+
));
41+
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
42+
43+
const AccordionContent = React.forwardRef<
44+
React.ElementRef<typeof AccordionPrimitive.Content>,
45+
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
46+
>(({ className, children, ...props }, ref) => (
47+
<AccordionPrimitive.Content
48+
ref={ref}
49+
className="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
50+
{...props}
51+
>
52+
<div className={cn("pt-0 pb-4", className)}>{children}</div>
53+
</AccordionPrimitive.Content>
54+
));
55+
56+
AccordionContent.displayName = AccordionPrimitive.Content.displayName;
57+
58+
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
"use client";
2+
3+
import {
4+
Accordion,
5+
AccordionContent,
6+
AccordionItem,
7+
AccordionTrigger,
8+
} from "@/components/ui/accordion";
9+
import { FormControl, Input, Textarea } from "@chakra-ui/react";
10+
import { OpenSeaPropertyBadge } from "components/badges/opensea";
11+
import { PropertiesFormControl } from "components/contract-pages/forms/properties.shared";
12+
import { FileInput } from "components/shared/FileInput";
13+
import { useImageFileOrUrl } from "hooks/useImageFileOrUrl";
14+
import type { UseFormReturn } from "react-hook-form";
15+
import { FormErrorMessage, FormHelperText, FormLabel } from "tw-components";
16+
import type { MetadataUploadFormValues } from "./Mintable";
17+
18+
export function MetadataUpload(props: {
19+
form: UseFormReturn<MetadataUploadFormValues>;
20+
}) {
21+
const {
22+
setValue,
23+
control,
24+
register,
25+
watch,
26+
formState: { errors },
27+
} = props.form;
28+
29+
const setFile = (file: File) => {
30+
if (file.type.includes("image")) {
31+
// image files
32+
setValue("image", file);
33+
if (watch("external_url") instanceof File) {
34+
setValue("external_url", undefined);
35+
}
36+
if (watch("animation_url") instanceof File) {
37+
setValue("animation_url", undefined);
38+
}
39+
} else if (
40+
["audio", "video", "text/html", "model/*"].some((type: string) =>
41+
file.type.includes(type),
42+
) ||
43+
file.name?.endsWith(".glb") ||
44+
file.name?.endsWith(".usdz") ||
45+
file.name?.endsWith(".gltf") ||
46+
file.name.endsWith(".obj")
47+
) {
48+
// audio, video, html, and glb (3d) files
49+
setValue("animation_url", file);
50+
if (watch("external_url") instanceof File) {
51+
setValue("external_url", undefined);
52+
}
53+
} else if (
54+
["text", "application/pdf"].some((type: string) =>
55+
file.type?.includes(type),
56+
)
57+
) {
58+
// text and pdf files
59+
setValue("external_url", file);
60+
if (watch("animation_url") instanceof File) {
61+
setValue("animation_url", undefined);
62+
}
63+
}
64+
};
65+
66+
const imageUrl = useImageFileOrUrl(watch("image") as File | string);
67+
const mediaFileUrl =
68+
watch("animation_url") instanceof File
69+
? watch("animation_url")
70+
: watch("external_url") instanceof File
71+
? watch("external_url")
72+
: watch("image") instanceof File
73+
? imageUrl
74+
: undefined;
75+
76+
const mediaFileError =
77+
watch("animation_url") instanceof File
78+
? errors?.animation_url
79+
: watch("external_url") instanceof File
80+
? errors?.external_url
81+
: watch("image") instanceof File
82+
? errors?.image
83+
: undefined;
84+
85+
const externalUrl = watch("external_url");
86+
const externalIsTextFile =
87+
externalUrl instanceof File &&
88+
(externalUrl.type.includes("text") || externalUrl.type.includes("pdf"));
89+
90+
const showCoverImageUpload =
91+
watch("animation_url") instanceof File ||
92+
watch("external_url") instanceof File;
93+
94+
return (
95+
<div className="flex flex-col gap-6">
96+
<FormControl isRequired isInvalid={!!errors.name}>
97+
<FormLabel>Name</FormLabel>
98+
<Input autoFocus {...register("name")} />
99+
<FormErrorMessage>{errors?.name?.message}</FormErrorMessage>
100+
</FormControl>
101+
<FormControl isInvalid={!!mediaFileError}>
102+
<FormLabel>Media</FormLabel>
103+
<div>
104+
<FileInput
105+
previewMaxWidth="200px"
106+
value={mediaFileUrl as File | string}
107+
showUploadButton
108+
showPreview={true}
109+
setValue={setFile}
110+
className="rounded border border-border transition-all duration-200"
111+
selectOrUpload="Upload"
112+
helperText="Media"
113+
/>
114+
</div>
115+
<FormHelperText>
116+
You can upload image, audio, video, html, text, pdf, and 3d model
117+
files here.
118+
</FormHelperText>
119+
<FormErrorMessage>
120+
{mediaFileError?.message as unknown as string}
121+
</FormErrorMessage>
122+
</FormControl>
123+
{showCoverImageUpload && (
124+
<FormControl isInvalid={!!errors.image}>
125+
<FormLabel>Cover Image</FormLabel>
126+
<FileInput
127+
previewMaxWidth="200px"
128+
accept={{ "image/*": [] }}
129+
value={imageUrl}
130+
showUploadButton
131+
setValue={(file) => setValue("image", file)}
132+
className="rounded border border-border transition-all"
133+
/>
134+
<FormHelperText>
135+
You can optionally upload an image as the cover of your NFT.
136+
</FormHelperText>
137+
<FormErrorMessage>
138+
{errors?.image?.message as unknown as string}
139+
</FormErrorMessage>
140+
</FormControl>
141+
)}
142+
<FormControl isInvalid={!!errors.description}>
143+
<FormLabel>Description</FormLabel>
144+
<Textarea {...register("description")} />
145+
<FormErrorMessage>{errors?.description?.message}</FormErrorMessage>
146+
</FormControl>
147+
<PropertiesFormControl
148+
watch={watch}
149+
// biome-ignore lint/suspicious/noExplicitAny: FIXME
150+
errors={errors as any}
151+
control={control}
152+
register={register}
153+
setValue={setValue}
154+
/>
155+
<Accordion
156+
type="single"
157+
collapsible={!(errors.background_color || errors.external_url)}
158+
>
159+
<AccordionItem value="advanced-options">
160+
<AccordionTrigger className="justify-between">
161+
Advanced Options
162+
</AccordionTrigger>
163+
<AccordionContent className="flex flex-col gap-6 px-0">
164+
<FormControl isInvalid={!!errors.background_color}>
165+
<FormLabel>
166+
Background Color <OpenSeaPropertyBadge />
167+
</FormLabel>
168+
<Input max="6" {...register("background_color")} />
169+
<FormHelperText>
170+
Must be a six-character hexadecimal with a pre-pended #.
171+
</FormHelperText>
172+
<FormErrorMessage>
173+
{errors?.background_color?.message}
174+
</FormErrorMessage>
175+
</FormControl>
176+
{!externalIsTextFile && (
177+
<FormControl isInvalid={!!errors.external_url}>
178+
<FormLabel>
179+
External URL <OpenSeaPropertyBadge />
180+
</FormLabel>
181+
<Input {...register("external_url")} />
182+
<FormHelperText>
183+
This is the URL that will appear below the asset&apos;s image
184+
on OpenSea and will allow users to leave OpenSea and view the
185+
item on your site.
186+
</FormHelperText>
187+
<FormErrorMessage>
188+
{errors?.external_url?.message as unknown as string}
189+
</FormErrorMessage>
190+
</FormControl>
191+
)}
192+
<FormControl isInvalid={!!errors.customImage}>
193+
<FormLabel>Image URL</FormLabel>
194+
<Input max="6" {...register("customImage")} />
195+
<FormHelperText>
196+
If you already have your NFT image preuploaded, you can set the
197+
URL or URI here.
198+
</FormHelperText>
199+
<FormErrorMessage>
200+
{errors?.customImage?.message}
201+
</FormErrorMessage>
202+
</FormControl>
203+
<FormControl isInvalid={!!errors.customAnimationUrl}>
204+
<FormLabel>Animation URL</FormLabel>
205+
<Input max="6" {...register("customAnimationUrl")} />
206+
<FormHelperText>
207+
If you already have your NFT Animation URL preuploaded, you can
208+
set the URL or URI here.
209+
</FormHelperText>
210+
<FormErrorMessage>
211+
{errors?.customAnimationUrl?.message}
212+
</FormErrorMessage>
213+
</FormControl>
214+
</AccordionContent>
215+
</AccordionItem>
216+
</Accordion>
217+
</div>
218+
);
219+
}

0 commit comments

Comments
 (0)