Skip to content

Commit

Permalink
Merge branch 'profiles-page' of github.com:guildxyz/guild.xyz into pr…
Browse files Browse the repository at this point in the history
…ofiles-page
  • Loading branch information
dominik-stumpf committed Aug 16, 2024
2 parents e099066 + cd4e136 commit b79f500
Show file tree
Hide file tree
Showing 26 changed files with 503 additions and 353 deletions.
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
49 changes: 49 additions & 0 deletions src/app/create-guild/_components/CreateGuildButton.tsx
Original file line number Diff line number Diff line change
@@ -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<CreateGuildFormType>()
const { onSubmit, isLoading } = useCreateGuild()

const { isWeb3Connected } = useWeb3ConnectionManager()
const setIsWalletSelectorModalOpen = useSetAtom(walletSelectorModalAtom)

return (
<div className="flex flex-col gap-2">
<Collapsible open={!isWeb3Connected}>
<CollapsibleContent>
<Button
colorScheme="info"
size="xl"
disabled={isWeb3Connected}
onClick={() => setIsWalletSelectorModalOpen(true)}
className="w-full"
>
Connect wallet
</Button>
</CollapsibleContent>
</Collapsible>

<Button
colorScheme="success"
size="xl"
isLoading={isLoading}
loadingText="Creating guild"
onClick={handleSubmit(onSubmit)}
disabled={!isWeb3Connected}
>
Create guild
</Button>
</div>
)
}

export { CreateGuildButton }
53 changes: 53 additions & 0 deletions src/app/create-guild/_components/CreateGuildCard.tsx
Original file line number Diff line number Diff line change
@@ -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<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>

<CreateGuildImageUploader />

<div className="mb-8 flex flex-col gap-4">
<FormField
control={control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Guild name</FormLabel>
<FormControl>
<Input size="lg" {...field} />
</FormControl>

<FormErrorMessage />
</FormItem>
)}
/>

<EmailFormField />
</div>

<CreateGuildButton />
</Card>
)
}

export { CreateGuildCard }
48 changes: 48 additions & 0 deletions src/app/create-guild/_components/CreateGuildFormProvider.tsx
Original file line number Diff line number Diff line change
@@ -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<CreateGuildFormType>({
mode: "all",
resolver: zodResolver(schemas.GuildCreationPayloadSchema),
defaultValues,
})

return <FormProvider {...methods}>{children}</FormProvider>
}

export { CreateGuildFormProvider }
120 changes: 120 additions & 0 deletions src/app/create-guild/_components/CreateGuildImageUploader.tsx
Original file line number Diff line number Diff line change
@@ -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<CreateGuildFormType>()
const imageUrl = useWatch({ control, name: "imageUrl" })

const [placeholder, setPlaceholder] = useState<string | null>(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<File>(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 (
<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"
disabled={isUploading}
{...getRootProps()}
>
<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"
/>
) : isDragActive ? (
<UploadSimple className="h-auto w-1/3" weight="bold" />
) : (
<Image className="h-auto w-1/3" weight="bold" />
)}
</Button>
</FormControl>

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

export { CreateGuildImageUploader }
54 changes: 54 additions & 0 deletions src/app/create-guild/_components/EmailFormField.tsx
Original file line number Diff line number Diff line change
@@ -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<CreateGuildFormType>()
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 (
<FormField
control={control}
name="contacts.0.contact"
render={({ field }) => (
<FormItem>
<FormLabel>Your e-mail</FormLabel>
<FormControl>
<Input size="lg" {...field} />
</FormControl>

<FormErrorMessage />
</FormItem>
)}
/>
)
}

export { EmailFormField }
Loading

0 comments on commit b79f500

Please sign in to comment.