From 9adf5713effc2e6929bcc23f175a37178821905d Mon Sep 17 00:00:00 2001 From: BrickheadJohnny <92519134+BrickheadJohnny@users.noreply.github.com> Date: Thu, 15 Aug 2024 21:41:12 +0200 Subject: [PATCH 1/2] feat(create-guild): migrate to Tailwind (#1444) * feat: better input styles * feat(Input.stories): add invalid state * fix(GuildSearchBar): remove `variant` * fix(Input): don't show margin for ErrorMessage when not rendered --------- Co-authored-by: valid --- package-lock.json | 8 +- package.json | 2 +- .../_components/CreateGuildButton.tsx | 49 +++++++ .../_components/CreateGuildCard.tsx | 53 ++++++++ .../_components/CreateGuildFormProvider.tsx | 48 +++++++ .../_components/CreateGuildImageUploader.tsx | 120 ++++++++++++++++++ .../_components/EmailFormField.tsx | 54 ++++++++ .../create-guild/_hooks/useCreateGuild.ts} | 36 ++---- src/app/create-guild/page.tsx | 39 ++++++ src/app/create-guild/types.ts | 6 + .../explorer/_components/GuildSearchBar.tsx | 1 - src/app/globals.css | 12 +- .../BasicInfo/components/ContactInfo.tsx | 2 +- .../create-guild/CreateGuildButton.tsx | 46 ------- .../create-guild/CreateGuildForm.tsx | 115 ----------------- .../IconSelector/IconSelector.tsx | 2 +- .../IconSelector/components/PhotoUploader.tsx | 51 +------- .../create-guild/IconSelector/utils.ts | 52 ++++++++ src/hooks/usePinata/usePinata.ts | 6 +- src/pages/create-guild.tsx | 89 ------------- src/v2/components/ui/Button.tsx | 2 +- src/v2/components/ui/Form.tsx | 8 +- src/v2/components/ui/Input.stories.ts | 10 ++ src/v2/components/ui/Input.tsx | 18 +-- tailwind.config.ts | 16 ++- 25 files changed, 491 insertions(+), 354 deletions(-) create mode 100644 src/app/create-guild/_components/CreateGuildButton.tsx create mode 100644 src/app/create-guild/_components/CreateGuildCard.tsx create mode 100644 src/app/create-guild/_components/CreateGuildFormProvider.tsx create mode 100644 src/app/create-guild/_components/CreateGuildImageUploader.tsx create mode 100644 src/app/create-guild/_components/EmailFormField.tsx rename src/{components/create-guild/hooks/useCreateGuild.tsx => app/create-guild/_hooks/useCreateGuild.ts} (76%) create mode 100644 src/app/create-guild/page.tsx create mode 100644 src/app/create-guild/types.ts delete mode 100644 src/components/create-guild/CreateGuildButton.tsx delete mode 100644 src/components/create-guild/CreateGuildForm.tsx create mode 100644 src/components/create-guild/IconSelector/utils.ts delete mode 100644 src/pages/create-guild.tsx diff --git a/package-lock.json b/package-lock.json index 682c33d4dd..36055050e8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,7 +19,7 @@ "@emotion/styled": "^11.11.0", "@fuels/connectors": "^0.5.0", "@fuels/react": "^0.20.0", - "@guildxyz/types": "^1.9.31", + "@guildxyz/types": "^1.9.38", "@hcaptcha/react-hcaptcha": "^1.4.4", "@hookform/resolvers": "^3.3.4", "@lexical/code": "^0.12.0", @@ -5466,9 +5466,9 @@ } }, "node_modules/@guildxyz/types": { - "version": "1.9.31", - "resolved": "https://registry.npmjs.org/@guildxyz/types/-/types-1.9.31.tgz", - "integrity": "sha512-/GhQKytwdFsjBrAV49fbQ7okyW9JhNmXo3S4sM2J2X7zSjw/zVSXIIOoQ7dpCZdzl1AudDBAVVSo699vKlscpQ==", + "version": "1.9.38", + "resolved": "https://registry.npmjs.org/@guildxyz/types/-/types-1.9.38.tgz", + "integrity": "sha512-cyGwit9QVnjoqQXf/XhiSOnevjDWnu4qeQQEVK319P1vvXxF/0zKDUBqYdgp62gkLG3wENDTKOaE5O4AB9dDMQ==", "license": "ISC", "dependencies": { "zod": "^3.22.4" diff --git a/package.json b/package.json index 8b996a43c7..3a19e8c7c9 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "@emotion/styled": "^11.11.0", "@fuels/connectors": "^0.5.0", "@fuels/react": "^0.20.0", - "@guildxyz/types": "^1.9.31", + "@guildxyz/types": "^1.9.38", "@hcaptcha/react-hcaptcha": "^1.4.4", "@hookform/resolvers": "^3.3.4", "@lexical/code": "^0.12.0", diff --git a/src/app/create-guild/_components/CreateGuildButton.tsx b/src/app/create-guild/_components/CreateGuildButton.tsx new file mode 100644 index 0000000000..438ac05f71 --- /dev/null +++ b/src/app/create-guild/_components/CreateGuildButton.tsx @@ -0,0 +1,49 @@ +"use client" + +import { walletSelectorModalAtom } from "@/components/Providers/atoms" +import { useWeb3ConnectionManager } from "@/components/Web3ConnectionManager/hooks/useWeb3ConnectionManager" +import { Button } from "@/components/ui/Button" +import { Collapsible, CollapsibleContent } from "@/components/ui/Collapsible" +import { useSetAtom } from "jotai" +import { useFormContext } from "react-hook-form" +import { useCreateGuild } from "../_hooks/useCreateGuild" +import { CreateGuildFormType } from "../types" + +const CreateGuildButton = () => { + const { handleSubmit } = useFormContext() + const { onSubmit, isLoading } = useCreateGuild() + + const { isWeb3Connected } = useWeb3ConnectionManager() + const setIsWalletSelectorModalOpen = useSetAtom(walletSelectorModalAtom) + + return ( +
+ + + + + + + +
+ ) +} + +export { CreateGuildButton } diff --git a/src/app/create-guild/_components/CreateGuildCard.tsx b/src/app/create-guild/_components/CreateGuildCard.tsx new file mode 100644 index 0000000000..762ed68654 --- /dev/null +++ b/src/app/create-guild/_components/CreateGuildCard.tsx @@ -0,0 +1,53 @@ +"use client" + +import { Card } from "@/components/ui/Card" +import { + FormControl, + FormErrorMessage, + FormField, + FormItem, + FormLabel, +} from "@/components/ui/Form" +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 } = useFormContext() + + return ( + +

+ Begin your guild +

+ + + +
+ ( + + Guild name + + + + + + + )} + /> + + +
+ + +
+ ) +} + +export { CreateGuildCard } diff --git a/src/app/create-guild/_components/CreateGuildFormProvider.tsx b/src/app/create-guild/_components/CreateGuildFormProvider.tsx new file mode 100644 index 0000000000..56cd60d9d9 --- /dev/null +++ b/src/app/create-guild/_components/CreateGuildFormProvider.tsx @@ -0,0 +1,48 @@ +"use client" + +import { schemas } from "@guildxyz/types" +import { zodResolver } from "@hookform/resolvers/zod" +import { PropsWithChildren } from "react" +import { FormProvider, useForm } from "react-hook-form" +import getRandomInt from "utils/getRandomInt" +import { CreateGuildFormType } from "../types" + +const defaultValues = { + name: "", + imageUrl: "", + contacts: [ + { + type: "EMAIL", + contact: "", + }, + ], + /** + * We need to define these values so the Zod resolver won't throw errors, but we'll actually overwrite the urlName with a proper value in the `useCreateGuild` hook + * + * Temporarily creating a default Member role, later the users will be able to pick from Guild Templates + */ + urlName: "", + roles: [ + { + name: "Member", + imageUrl: `/guildLogos/${getRandomInt(286)}.svg`, + requirements: [ + { + type: "FREE", + }, + ], + }, + ], +} satisfies CreateGuildFormType + +const CreateGuildFormProvider = ({ children }: PropsWithChildren) => { + const methods = useForm({ + mode: "all", + resolver: zodResolver(schemas.GuildCreationPayloadSchema), + defaultValues, + }) + + return {children} +} + +export { CreateGuildFormProvider } diff --git a/src/app/create-guild/_components/CreateGuildImageUploader.tsx b/src/app/create-guild/_components/CreateGuildImageUploader.tsx new file mode 100644 index 0000000000..18acebbfc8 --- /dev/null +++ b/src/app/create-guild/_components/CreateGuildImageUploader.tsx @@ -0,0 +1,120 @@ +import { Button } from "@/components/ui/Button" +import { FormControl, FormErrorMessage, FormItem } from "@/components/ui/Form" +import { Image, Spinner, UploadSimple } from "@phosphor-icons/react/dist/ssr" +import { + convertFilesFromEvent, + 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() + const imageUrl = useWatch({ control, name: "imageUrl" }) + + const [placeholder, setPlaceholder] = useState(null) + const { fileRejections, getRootProps, getInputProps, isDragActive } = 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 = await convertFilesFromEvent(event) + + const filePromises = [] + + for (const file of filesFromEvent) { + filePromises.push( + new Promise(async (resolve) => { + if (file.type.includes("svg")) { + resolve(file) + } else { + const { width, height } = await getWidthAndHeightFromFile(file) + Object.defineProperty(file, "width", { value: width }) + Object.defineProperty(file, "height", { value: 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 ( + + + + + + + {fileRejectionError?.code in ERROR_MESSAGES + ? ERROR_MESSAGES[fileRejectionError.code as keyof typeof ERROR_MESSAGES] + : fileRejectionError?.message} + + + ) +} + +export { CreateGuildImageUploader } diff --git a/src/app/create-guild/_components/EmailFormField.tsx b/src/app/create-guild/_components/EmailFormField.tsx new file mode 100644 index 0000000000..01f780436c --- /dev/null +++ b/src/app/create-guild/_components/EmailFormField.tsx @@ -0,0 +1,54 @@ +import { + FormControl, + FormErrorMessage, + FormField, + FormItem, + FormLabel, +} from "@/components/ui/Form" +import { Input } from "@/components/ui/Input" +import useUser from "components/[guild]/hooks/useUser" +import { useEffect } from "react" +import { useFormContext, useWatch } from "react-hook-form" +import { CreateGuildFormType } from "../types" + +const EmailFormField = () => { + const { + control, + setValue, + formState: { touchedFields }, + } = useFormContext() + const { emails, platformUsers } = useUser() + + const providedEmail = useWatch({ control, name: "contacts.0.contact" }) + useEffect(() => { + if (!!providedEmail || touchedFields.contacts?.[0]?.contact) return + + const emailAddress = emails?.emailAddress + const googleEmailAddress = platformUsers?.find( + (pu) => pu.platformName === "GOOGLE" + )?.platformUserId + + if (!emailAddress && !googleEmailAddress) return + + setValue("contacts.0.contact", emailAddress ?? googleEmailAddress) + }, [touchedFields.contacts, emails, platformUsers, providedEmail, setValue]) + + return ( + ( + + Your e-mail + + + + + + + )} + /> + ) +} + +export { EmailFormField } diff --git a/src/components/create-guild/hooks/useCreateGuild.tsx b/src/app/create-guild/_hooks/useCreateGuild.ts similarity index 76% rename from src/components/create-guild/hooks/useCreateGuild.tsx rename to src/app/create-guild/_hooks/useCreateGuild.ts index feefdb0f04..4a7136f788 100644 --- a/src/components/create-guild/hooks/useCreateGuild.tsx +++ b/src/app/create-guild/_hooks/useCreateGuild.ts @@ -1,4 +1,5 @@ import { usePostHogContext } from "@/components/Providers/PostHogProvider" +import { useToast } from "@/components/ui/hooks/useToast" import { useYourGuilds } from "@/hooks/useYourGuilds" import { Schemas } from "@guildxyz/types" import processConnectorError from "components/[guild]/JoinModal/utils/processConnectorError" @@ -6,13 +7,12 @@ import useJsConfetti from "components/create-guild/hooks/useJsConfetti" import useMatchMutate from "hooks/useMatchMutate" import useShowErrorToast from "hooks/useShowErrorToast" import { SignedValidation, useSubmitWithSign } from "hooks/useSubmit" -import useToast from "hooks/useToast" -import { useRouter } from "next/router" +import { useRouter } from "next/navigation" import { Guild, GuildBase } from "types" import fetcher from "utils/fetcher" import getRandomInt from "utils/getRandomInt" import slugify from "utils/slugify" -import { CreateGuildFormType } from "../CreateGuildForm" +import { CreateGuildFormType } from "../types" const useCreateGuild = ({ onError, @@ -26,9 +26,9 @@ const useCreateGuild = ({ const { mutate: mutateYourGuilds } = useYourGuilds() const matchMutate = useMatchMutate() - const toast = useToast() + const { toast } = useToast() const showErrorToast = useShowErrorToast() - const triggerConfetti = useJsConfetti() + const triggerConfetti = useJsConfetti() // TODO: use the new confetti? const router = useRouter() const fetchData = async (signedValidation: SignedValidation): Promise => @@ -61,9 +61,9 @@ const useCreateGuild = ({ ) toast({ - title: `Guild successfully created!`, + title: "Guild successfully created!", description: "You're being redirected to its page", - status: "success", + variant: "success", }) onSuccess?.() router.push(`/${response_.urlName}`) @@ -72,32 +72,18 @@ const useCreateGuild = ({ return { ...useSubmitResponse, - /** - * Temporarily creating a default Member role, later the users will be able to - * pick from Guild Templates - */ + onSubmit: (data: CreateGuildFormType) => useSubmitResponse.onSubmit({ ...data, urlName: slugify(data.name), imageUrl: data.imageUrl || `/guildLogos/${getRandomInt(286)}.svg`, - roles: [ - { - name: "Member", - imageUrl: `/guildLogos/${getRandomInt(286)}.svg`, - requirements: [ - { - type: "FREE", - }, - ], - }, - ], } satisfies Schemas["GuildCreationPayload"]), } } -const mutateGuildsCache = (prev: GuildBase[], createdGuild: Guild) => [ - ...prev, +const mutateGuildsCache = (prev: GuildBase[] | undefined, createdGuild: Guild) => [ + ...(prev ?? []), { id: createdGuild.id, name: createdGuild.name, @@ -110,4 +96,4 @@ const mutateGuildsCache = (prev: GuildBase[], createdGuild: Guild) => [ }, ] -export default useCreateGuild +export { useCreateGuild } diff --git a/src/app/create-guild/page.tsx b/src/app/create-guild/page.tsx new file mode 100644 index 0000000000..0210554ca8 --- /dev/null +++ b/src/app/create-guild/page.tsx @@ -0,0 +1,39 @@ +import { Header } from "@/components/Header" +import { Layout, LayoutBanner, LayoutHero, LayoutMain } from "@/components/Layout" +import svgToTinyDataUri from "mini-svg-data-uri" +import { CreateGuildCard } from "./_components/CreateGuildCard" +import { CreateGuildFormProvider } from "./_components/CreateGuildFormProvider" + +export const metadata = { + title: "Begin your guild", +} + +const Page = () => ( + + +
` + )}")`, + }} + /> + + + +
+
+ + +
+ + + + + + + +) + +export default Page diff --git a/src/app/create-guild/types.ts b/src/app/create-guild/types.ts new file mode 100644 index 0000000000..11cb102897 --- /dev/null +++ b/src/app/create-guild/types.ts @@ -0,0 +1,6 @@ +import { Schemas } from "@guildxyz/types" + +export type CreateGuildFormType = Pick< + Schemas["GuildCreationPayload"], + "name" | "urlName" | "imageUrl" | "contacts" | "roles" | "theme" +> diff --git a/src/app/explorer/_components/GuildSearchBar.tsx b/src/app/explorer/_components/GuildSearchBar.tsx index a78511a9ad..b3561c29ce 100644 --- a/src/app/explorer/_components/GuildSearchBar.tsx +++ b/src/app/explorer/_components/GuildSearchBar.tsx @@ -40,7 +40,6 @@ export const GuildSearchBar = () => {
setSearch(currentTarget.value)} value={search} diff --git a/src/app/globals.css b/src/app/globals.css index ceea35c14a..da6a3ca248 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -72,7 +72,10 @@ --border: 240 5.9% 90%; --border-muted: 240 3% 93%; - --input: var(--border); + --input-border: 240, 6%, 90%, 100%; + --input-border-accent: 240, 5%, 84%, 100%; + --input-border-invalid: 0 84% 60%; + --input-background: 0, 0%, 100%, 100%; --ring: 240, 0%, 45%, 25%; @@ -149,7 +152,7 @@ --secondary-subtle: 240 6% 90%; --secondary-subtle-foreground: var(--foreground); - --info: 217 91 60%; + --info: 217 91% 60%; --info-hover: 213 94% 68%; --info-active: 212 96% 78%; --info-foreground: 0 0% 100%; @@ -183,7 +186,10 @@ --border: 240 3% 38%; --border-muted: 240 3% 28%; - --input: var(--border); + --input-border: 0, 0%, 100%, 16%; + --input-border-accent: 0, 0%, 100%, 24%; + --input-border-invalid: 0 94% 82%; + --input-background: 0, 0%, 0%, 16%; --ring: 240 0% 45%; diff --git a/src/components/create-guild/BasicInfo/components/ContactInfo.tsx b/src/components/create-guild/BasicInfo/components/ContactInfo.tsx index 61774aa32b..61e9ae8400 100644 --- a/src/components/create-guild/BasicInfo/components/ContactInfo.tsx +++ b/src/components/create-guild/BasicInfo/components/ContactInfo.tsx @@ -12,11 +12,11 @@ import { Text, } from "@chakra-ui/react" import { ArrowSquareOut, Plus, TrashSimple } from "@phosphor-icons/react" +import { CreateGuildFormType } from "app/create-guild/types" import Button from "components/common/Button" import ClientOnly from "components/common/ClientOnly" import FormErrorMessage from "components/common/FormErrorMessage" import StyledSelect from "components/common/StyledSelect" -import { CreateGuildFormType } from "components/create-guild/CreateGuildForm" import { Controller, useFieldArray, useFormContext } from "react-hook-form" import { SelectOption } from "types" diff --git a/src/components/create-guild/CreateGuildButton.tsx b/src/components/create-guild/CreateGuildButton.tsx deleted file mode 100644 index 3fd5675e2d..0000000000 --- a/src/components/create-guild/CreateGuildButton.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { walletSelectorModalAtom } from "@/components/Providers/atoms" -import { useWeb3ConnectionManager } from "@/components/Web3ConnectionManager/hooks/useWeb3ConnectionManager" -import { Collapse, Stack } from "@chakra-ui/react" -import Button from "components/common/Button" -import { useSetAtom } from "jotai" -import { useFormContext } from "react-hook-form" -import { CreateGuildFormType } from "./CreateGuildForm" -import useCreateGuild from "./hooks/useCreateGuild" - -const CreateGuildButton = () => { - const { handleSubmit } = useFormContext() - const { onSubmit, isLoading } = useCreateGuild() - - const { isWeb3Connected } = useWeb3ConnectionManager() - const setIsWalletSelectorModalOpen = useSetAtom(walletSelectorModalAtom) - - return ( - - - - - - - ) -} - -export default CreateGuildButton diff --git a/src/components/create-guild/CreateGuildForm.tsx b/src/components/create-guild/CreateGuildForm.tsx deleted file mode 100644 index 78fee1a47c..0000000000 --- a/src/components/create-guild/CreateGuildForm.tsx +++ /dev/null @@ -1,115 +0,0 @@ -import { - Center, - FormControl, - FormLabel, - Heading, - Input, - Stack, -} from "@chakra-ui/react" -import { Schemas } from "@guildxyz/types" -import useUser from "components/[guild]/hooks/useUser" -import Card from "components/common/Card" -import FormErrorMessage from "components/common/FormErrorMessage" -import usePinata from "hooks/usePinata" -import { useEffect } from "react" -import { useFormContext, useWatch } from "react-hook-form" -import getColorByImage from "utils/getColorByImage" -import CreateGuildButton from "./CreateGuildButton" -import IconSelector from "./IconSelector" -import Name from "./Name" - -export type CreateGuildFormType = Pick< - Schemas["GuildCreationPayload"], - "name" | "imageUrl" | "contacts" | "theme" -> - -const CreateGuildForm = () => { - const { - control, - register, - setValue, - formState: { errors, touchedFields }, - } = useFormContext() - - const iconUploader = usePinata({ - fieldToSetOnSuccess: "imageUrl", - fieldToSetOnError: "imageUrl", - control, - }) - - const { emails, platformUsers } = useUser() - - const providedEmail = useWatch({ control, name: "contacts.0.contact" }) - useEffect(() => { - if (!!providedEmail || touchedFields.contacts?.[0]?.contact) return - - const emailAddress = emails?.emailAddress - const googleEmailAddress = platformUsers?.find( - (pu) => pu.platformName === "GOOGLE" - )?.platformUserId - - if (!emailAddress && !googleEmailAddress) return - - setValue("contacts.0.contact", emailAddress ?? googleEmailAddress) - }, [touchedFields.contacts, emails, platformUsers, providedEmail, setValue]) - - return ( - - - - Begin your guild - - -
- { - const generatedThemeColor = await getColorByImage(objectURL) - setValue("theme.color", generatedThemeColor) - }} - boxSize={28} - /> -
- - - Guild name - - - - - Your email - - - {errors.contacts?.[0]?.contact?.message} - - - - -
-
- ) -} - -export default CreateGuildForm diff --git a/src/components/create-guild/IconSelector/IconSelector.tsx b/src/components/create-guild/IconSelector/IconSelector.tsx index aada9e673d..59ac5cf266 100644 --- a/src/components/create-guild/IconSelector/IconSelector.tsx +++ b/src/components/create-guild/IconSelector/IconSelector.tsx @@ -24,13 +24,13 @@ import { useRadioGroup, } from "@chakra-ui/react" import { Image } from "@phosphor-icons/react" +import { CreateGuildFormType } from "app/create-guild/types" import LogicDivider from "components/[guild]/LogicDivider" import GuildLogo from "components/common/GuildLogo" import { Modal } from "components/common/Modal" import { Uploader } from "hooks/usePinata/usePinata" import React, { ComponentProps, useEffect } from "react" import { useController, useFormContext } from "react-hook-form" -import { CreateGuildFormType } from "../CreateGuildForm" import PhotoUploader from "./components/PhotoUploader" import SelectorButton from "./components/SelectorButton" import icons from "./icons.json" diff --git a/src/components/create-guild/IconSelector/components/PhotoUploader.tsx b/src/components/create-guild/IconSelector/components/PhotoUploader.tsx index 850fa696c8..58d8e9653f 100644 --- a/src/components/create-guild/IconSelector/components/PhotoUploader.tsx +++ b/src/components/create-guild/IconSelector/components/PhotoUploader.tsx @@ -5,8 +5,8 @@ import FormErrorMessage from "components/common/FormErrorMessage" import GuildLogo from "components/common/GuildLogo" import useDropzone, { ERROR_MESSAGES } from "hooks/useDropzone" import { Uploader } from "hooks/usePinata/usePinata" -import { FileError } from "react-dropzone" import { useFormContext, useWatch } from "react-hook-form" +import { getWidthAndHeightFromFile, imageDimensionsValidator } from "../utils" type Props = { uploader: Uploader @@ -16,46 +16,6 @@ type Props = { onGeneratedBlobChange?: (objectURL: string) => void } -type FileWithWidthandHeight = File & { width: number; height: number } - -const getWidthAndHeightFromFile = ( - file: File -): Promise<{ width: number; height: number }> => - new Promise((resolve) => { - const dataURL = URL.createObjectURL(file) - const img = new Image() - img.onload = () => { - const { width, height } = img - URL.revokeObjectURL(dataURL) - resolve({ - width, - height, - }) - } - img.src = dataURL - }) - -const imageDimensionsValidator = ( - file: FileWithWidthandHeight, - minW: number, - minH: number -): FileError => { - if (typeof file.width !== "number" && typeof file.height !== "number") return null - - if (file.width < minW || file.height < minH) - return { - code: "dimension-too-small", - message: getDimensionErrorMessage(minW, minH), - } - - return null -} - -const getDimensionErrorMessage = (minW?: number, minH?: number): string => { - if (minW && minH) return `Image should be at least ${minW}x${minH}px` - return `Image ${minW ? "width" : "height"} should be at least ${minW || minH}px` -} - const PhotoUploader = ({ uploader: { onUpload, isUploading }, closeModal, @@ -80,7 +40,7 @@ const PhotoUploader = ({ for (const file of filesFromEvent) { filePromises.push( - new Promise(async (resolve) => { + new Promise(async (resolve) => { if (file.type.includes("svg")) { resolve(file) } else { @@ -97,12 +57,7 @@ const PhotoUploader = ({ return files }, validator: (file) => - (minW || minH) && - imageDimensionsValidator( - file as unknown as FileWithWidthandHeight, - minW, - minH - ), + minW || minH ? imageDimensionsValidator(file, minW ?? 0, minH ?? 0) : null, onDrop: (accepted) => { if (accepted.length > 0) { const generatedBlob = URL.createObjectURL(accepted[0]) diff --git a/src/components/create-guild/IconSelector/utils.ts b/src/components/create-guild/IconSelector/utils.ts new file mode 100644 index 0000000000..8513d3d85d --- /dev/null +++ b/src/components/create-guild/IconSelector/utils.ts @@ -0,0 +1,52 @@ +import { FileError } from "react-dropzone" + +export const getWidthAndHeightFromFile = ( + file: File +): Promise<{ width: number; height: number }> => + new Promise((resolve) => { + const dataURL = URL.createObjectURL(file) + const img = new Image() + img.onload = () => { + const { width, height } = img + URL.revokeObjectURL(dataURL) + resolve({ + width, + height, + }) + } + img.src = dataURL + }) + +export const imageDimensionsValidator = ( + file: File & { width?: number; height?: number }, + minW: number, + minH: number +): FileError | null => { + const { width = 0, height = 0 } = file + + if (width < minW || height < minH) + return { + code: "dimension-too-small", + message: getDimensionErrorMessage(minW, minH), + } + + return null +} + +export const getDimensionErrorMessage = (minW?: number, minH?: number): string => { + if (minW && minH) return `Image should be at least ${minW}x${minH}px` + return `Image ${minW ? "width" : "height"} should be at least ${minW || minH}px` +} + +export const convertFilesFromEvent = async (event: any) => { + // This happens in Chromium after choosing image with click. Event is an array of FileSystemHandle objects + if (Array.isArray(event)) { + return await Promise.all(event.map(async (file) => await file.getFile())) + } + + // This happens on choosing image with drag & drop + if (event.dataTransfer) return event.dataTransfer.files + + // This happens in Firefox on choosing image with click + return event.target.files +} diff --git a/src/hooks/usePinata/usePinata.ts b/src/hooks/usePinata/usePinata.ts index cadd574557..766ddca737 100644 --- a/src/hooks/usePinata/usePinata.ts +++ b/src/hooks/usePinata/usePinata.ts @@ -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" @@ -29,7 +29,7 @@ const usePinata = ({ fieldToSetOnError = "" as Path, control: controlFromProps, }: Props = {}): Uploader => { - const toast = useToast() + const { toast } = useToast() const { control: controlFromContext } = useFormContext() ?? {} const control = controlFromContext ?? controlFromProps @@ -57,7 +57,7 @@ const usePinata = ({ : undefined toast({ - status: "error", + variant: "error", title: "Failed to upload image", description, }) diff --git a/src/pages/create-guild.tsx b/src/pages/create-guild.tsx deleted file mode 100644 index 15e4c0a645..0000000000 --- a/src/pages/create-guild.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import { Box, useColorModeValue } from "@chakra-ui/react" -import { ThemeProvider } from "components/[guild]/ThemeContext" -import ClientOnly from "components/common/ClientOnly" -import { Layout } from "components/common/Layout" -import CreateGuildForm, { - CreateGuildFormType, -} from "components/create-guild/CreateGuildForm" -import DynamicDevTool from "components/create-guild/DynamicDevTool" -import svgToTinyDataUri from "mini-svg-data-uri" -import { FormProvider, useForm } from "react-hook-form" - -const CreateGuildPage = (): JSX.Element => { - const methods = useForm({ - mode: "all", - defaultValues: { - name: "", - imageUrl: "", - contacts: [ - { - type: "EMAIL", - contact: "", - }, - ], - }, - }) - - const bgColor = useColorModeValue("var(--chakra-colors-gray-800)", "#1d1d1f") - const bgOpacity = useColorModeValue(0.06, 0.06) - const pageBgColor = useColorModeValue( - "var(--chakra-colors-gray-100)", - "var(--chakra-colors-gray-800)" - ) - const bgPatternColor = useColorModeValue("#c5c5ca", "#52525b") - - return ( - <> - - ` - )}")`} - bgPosition="top 16px left 0px" - minH="100vh" - > - - - - - - - - - - - - - - - - - ) -} - -const CreateGuildPageWrapper = (): JSX.Element => ( - - - -) - -export default CreateGuildPageWrapper diff --git a/src/v2/components/ui/Button.tsx b/src/v2/components/ui/Button.tsx index af798f8d07..69c8d3bd2f 100644 --- a/src/v2/components/ui/Button.tsx +++ b/src/v2/components/ui/Button.tsx @@ -5,7 +5,7 @@ import { type VariantProps, cva } from "class-variance-authority" import { ButtonHTMLAttributes, forwardRef } from "react" const buttonVariants = cva( - "font-semibold inline-flex items-center justify-center whitespace-nowrap transition-colors focus-visible:outline-none focus-visible:ring-4 focus:ring-ring disabled:pointer-events-none disabled:opacity-50 rounded-xl text-base text-ellipsis overflow-hidden gap-1.5", + "font-semibold inline-flex items-center justify-center whitespace-nowrap transition-colors focus-visible:outline-none focus-visible:ring-4 focus:ring-ring disabled:pointer-events-none disabled:opacity-50 rounded-xl text-base text-ellipsis overflow-hidden gap-1.5 cursor-pointer", { variants: { variant: { diff --git a/src/v2/components/ui/Form.tsx b/src/v2/components/ui/Form.tsx index 849886a5bc..d47fc5d9c1 100644 --- a/src/v2/components/ui/Form.tsx +++ b/src/v2/components/ui/Form.tsx @@ -80,7 +80,7 @@ const FormItem = forwardRef>( return ( -
+
) } @@ -96,7 +96,7 @@ const FormLabel = forwardRef< return (