Skip to content

Commit

Permalink
feat: implement CreateGuildImageUploader
Browse files Browse the repository at this point in the history
  • Loading branch information
BrickheadJohnny committed Aug 14, 2024
1 parent 09548bb commit cb1ddd5
Show file tree
Hide file tree
Showing 4 changed files with 126 additions and 20 deletions.
19 changes: 3 additions & 16 deletions src/app/create-guild/_components/CreateGuildCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,32 +12,19 @@ import { Input } from "@/components/ui/Input"
import { useFormContext } from "react-hook-form"
import { CreateGuildFormType } from "../types"
import { CreateGuildButton } from "./CreateGuildButton"
import { CreateGuildImageUploader } from "./CreateGuildImageUploader"
import { EmailFormField } from "./EmailFormField"

const CreateGuildCard = () => {
const {
control,
formState: { errors },
} = useFormContext<CreateGuildFormType>()
const { control } = useFormContext<CreateGuildFormType>()

return (
<Card className="flex flex-col px-5 py-6 shadow-lg md:px-6">
<h2 className="mb-7 text-center font-display font-extrabold text-2xl">
Begin your guild
</h2>

{/* <Center mb="6">
<IconSelector
uploader={iconUploader}
minW={512}
minH={512}
onGeneratedBlobChange={async (objectURL) => {
const generatedThemeColor = await getColorByImage(objectURL)
setValue("theme.color", generatedThemeColor)
}}
boxSize={28}
/>
</Center> */}
<CreateGuildImageUploader />

<div className="mb-8 flex flex-col gap-4">
<FormField
Expand Down
119 changes: 119 additions & 0 deletions src/app/create-guild/_components/CreateGuildImageUploader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { Button } from "@/components/ui/Button"
import { FormControl, FormErrorMessage, FormItem } from "@/components/ui/Form"
import { Image, Spinner } from "@phosphor-icons/react/dist/ssr"
import {
getWidthAndHeightFromFile,
imageDimensionsValidator,
} from "components/create-guild/IconSelector/utils"
import useDropzone, { ERROR_MESSAGES } from "hooks/useDropzone"
import usePinata from "hooks/usePinata"
import { useState } from "react"
import { useFormContext, useWatch } from "react-hook-form"
import { CreateGuildFormType } from "../types"

const MIN_WIDTH = 512
const MIN_HEIGHT = 512

/**
* This is a pretty specific component right now, but we should generalise it once we start using it in other places too (e.g. on the create profile page)
*/
const CreateGuildImageUploader = () => {
const { control } = useFormContext<CreateGuildFormType>()
const imageUrl = useWatch({ control, name: "imageUrl" })

const [placeholder, setPlaceholder] = useState<string | null>(null)
const { fileRejections, getRootProps, getInputProps } = useDropzone({
multiple: false,
noClick: false,
// We need to use any here unfortunately, but this is the correct usage according to the react-dropzone source code
getFilesFromEvent: async (event: any) => {
const filesFromEvent = event.dataTransfer
? event.dataTransfer.files
: event.target.files

const filePromises = []

for (const file of filesFromEvent) {
filePromises.push(
new Promise<File>(async (resolve) => {
if (file.type.includes("svg")) {
resolve(file)
} else {
const { width, height } = await getWidthAndHeightFromFile(file)
file.width = width
file.height = height
resolve(file)
}
})
)
}

const files = await Promise.all(filePromises)
return files
},
validator: (file) =>
MIN_WIDTH || MIN_HEIGHT
? imageDimensionsValidator(file, MIN_WIDTH ?? 0, MIN_HEIGHT ?? 0)
: null,
onDrop: (accepted) => {
if (accepted.length > 0) {
const generatedBlob = URL.createObjectURL(accepted[0])
setPlaceholder(generatedBlob)
onUpload({ data: [accepted[0]] })
}
},
})

const fileRejectionError = fileRejections?.[0]?.errors?.[0]

const { onUpload, isUploading } = usePinata({
control,
fieldToSetOnError: "imageUrl",
fieldToSetOnSuccess: "imageUrl",
})

return (
<FormItem className="mb-6 flex flex-col items-center justify-center">
<FormControl className="size-28 rounded-full bg-input-background">
<Button
variant="ghost"
className="relative size-28 rounded-full border border-input-border p-0 disabled:opacity-100"
{...getRootProps()}
disabled={isUploading}
>
<input {...getInputProps()} hidden />
{isUploading ? (
<>
{placeholder && (
<div className="absolute inset-0">
<img
src={placeholder}
alt="Uploading image..."
className="size-full object-cover opacity-50"
/>
</div>
)}
<Spinner weight="bold" className="size-1/3 animate-spin" />
</>
) : imageUrl ? (
<img
src={imageUrl}
alt="Guild image"
className="size-full object-cover"
/>
) : (
<Image className="h-auto w-1/3" />
)}
</Button>
</FormControl>

<FormErrorMessage>
{fileRejectionError?.code in ERROR_MESSAGES
? ERROR_MESSAGES[fileRejectionError.code as keyof typeof ERROR_MESSAGES]
: fileRejectionError?.message}
</FormErrorMessage>
</FormItem>
)
}

export { CreateGuildImageUploader }
6 changes: 3 additions & 3 deletions src/hooks/usePinata/usePinata.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useToast } from "@/components/ui/hooks/useToast"
import { env } from "env"
import useSubmit from "hooks/useSubmit"
import useToast from "hooks/useToast"
import { useCallback } from "react"
import { Control, Path, useController, useFormContext } from "react-hook-form"
import getRandomInt from "utils/getRandomInt"
Expand Down Expand Up @@ -29,7 +29,7 @@ const usePinata = <TFieldValues, TContext>({
fieldToSetOnError = "" as Path<TFieldValues>,
control: controlFromProps,
}: Props<TFieldValues, TContext> = {}): Uploader => {
const toast = useToast()
const { toast } = useToast()
const { control: controlFromContext } = useFormContext<TFieldValues>() ?? {}
const control = controlFromContext ?? controlFromProps

Expand Down Expand Up @@ -57,7 +57,7 @@ const usePinata = <TFieldValues, TContext>({
: undefined

toast({
status: "error",
variant: "error",
title: "Failed to upload image",
description,
})
Expand Down
2 changes: 1 addition & 1 deletion src/v2/components/ui/Form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ const FormErrorMessage = forwardRef<
const [debounceBody] = useDebounceValue(body, 200)

return (
<Collapsible open={!!error}>
<Collapsible open={!!body}>
<CollapsibleContent>
<p
ref={ref}
Expand Down

0 comments on commit cb1ddd5

Please sign in to comment.