From b5ba49bfd6ae5a0d99932b9bba1267bca8c4d787 Mon Sep 17 00:00:00 2001 From: Dominik Stumpf Date: Fri, 6 Sep 2024 18:01:44 +0200 Subject: [PATCH 1/8] feat: move farcaster avatars to pinata on profile update --- .../(onboarding)/_components/StartProfile.tsx | 41 ++++++++++++------- 1 file changed, 27 insertions(+), 14 deletions(-) diff --git a/src/app/(marketing)/create-profile/(onboarding)/_components/StartProfile.tsx b/src/app/(marketing)/create-profile/(onboarding)/_components/StartProfile.tsx index 0bda7fe98f..02dde896fe 100644 --- a/src/app/(marketing)/create-profile/(onboarding)/_components/StartProfile.tsx +++ b/src/app/(marketing)/create-profile/(onboarding)/_components/StartProfile.tsx @@ -20,7 +20,7 @@ import { ArrowRight } from "@phosphor-icons/react/dist/ssr" import useUser from "components/[guild]/hooks/useUser" import usePinata from "hooks/usePinata" import useSubmitWithUpload from "hooks/useSubmitWithUpload" -import { useEffect, useState } from "react" +import { useEffect, useRef, useState } from "react" import { FormProvider, useForm } from "react-hook-form" import { useCreateProfile } from "../_hooks/useCreateProfile" import { CreateProfileStep } from "../types" @@ -37,19 +37,6 @@ export const StartProfile: CreateProfileStep = ({ data: chainData }) => { farcasterProfile ? CreateMethod.FillByFarcaster : undefined ) - useEffect(() => { - if (!farcasterProfile) return - setMethod(CreateMethod.FillByFarcaster) - form.setValue( - "name", - farcasterProfile.username ?? form.getValues()?.name ?? "", - { shouldValidate: true } - ) - form.setValue("profileImageUrl", farcasterProfile.avatar, { - shouldValidate: true, - }) - }, [farcasterProfile]) - const form = useForm({ resolver: zodResolver( schemas.ProfileCreationSchema.omit({ referrerUserId: true }) @@ -84,6 +71,32 @@ export const StartProfile: CreateProfileStep = ({ data: chainData }) => { control: form.control, fieldToSetOnSuccess: "profileImageUrl", }) + const isFarcasterAvatarUploaded = useRef(false) + + useEffect(() => { + if (!farcasterProfile) return + setMethod(CreateMethod.FillByFarcaster) + form.setValue( + "name", + farcasterProfile.username ?? form.getValues()?.name ?? "", + { shouldValidate: true } + ) + form.setValue("profileImageUrl", farcasterProfile.avatar, { + shouldValidate: true, + }) + + void (async function () { + if (!farcasterProfile.avatar || isFarcasterAvatarUploaded.current) return + const data = await (await fetch(farcasterProfile.avatar)).blob() + const fileName = new URL(farcasterProfile.avatar).pathname.split("/").at(-1) + if (!fileName) return + isFarcasterAvatarUploaded.current = true + profilePicUploader.onUpload({ + data: [new File([data], fileName)], + fileNames: [fileName], + }) + })() + }, [farcasterProfile, profilePicUploader.onUpload]) const { handleSubmit, isUploadingShown, uploadLoadingText } = useSubmitWithUpload( form.handleSubmit(onSubmit), From 0689b674912ec18af7a063571b6ca34ce6615011 Mon Sep 17 00:00:00 2001 From: Dominik Stumpf Date: Fri, 6 Sep 2024 18:20:16 +0200 Subject: [PATCH 2/8] chore: restrict submit when uploading image --- .../(onboarding)/_components/StartProfile.tsx | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/app/(marketing)/create-profile/(onboarding)/_components/StartProfile.tsx b/src/app/(marketing)/create-profile/(onboarding)/_components/StartProfile.tsx index 02dde896fe..26256fc44d 100644 --- a/src/app/(marketing)/create-profile/(onboarding)/_components/StartProfile.tsx +++ b/src/app/(marketing)/create-profile/(onboarding)/_components/StartProfile.tsx @@ -2,7 +2,6 @@ import FarcasterImage from "@/../static/socialIcons/farcaster.svg" import { ConnectFarcasterButton } from "@/components/Account/components/AccountModal/components/FarcasterProfile" -import {} from "@/components/ui/Avatar" import { Button } from "@/components/ui/Button" import { FormControl, @@ -15,7 +14,6 @@ import { Input } from "@/components/ui/Input" import { EditProfilePicture } from "@app/(marketing)/profile/_components/EditProfile/EditProfilePicture" import { Schemas, schemas } from "@guildxyz/types" import { zodResolver } from "@hookform/resolvers/zod" -import {} from "@phosphor-icons/react" import { ArrowRight } from "@phosphor-icons/react/dist/ssr" import useUser from "components/[guild]/hooks/useUser" import usePinata from "hooks/usePinata" @@ -81,9 +79,6 @@ export const StartProfile: CreateProfileStep = ({ data: chainData }) => { farcasterProfile.username ?? form.getValues()?.name ?? "", { shouldValidate: true } ) - form.setValue("profileImageUrl", farcasterProfile.avatar, { - shouldValidate: true, - }) void (async function () { if (!farcasterProfile.avatar || isFarcasterAvatarUploaded.current) return @@ -96,7 +91,7 @@ export const StartProfile: CreateProfileStep = ({ data: chainData }) => { fileNames: [fileName], }) })() - }, [farcasterProfile, profilePicUploader.onUpload]) + }, [farcasterProfile, profilePicUploader.onUpload, form.setValue, form.getValues]) const { handleSubmit, isUploadingShown, uploadLoadingText } = useSubmitWithUpload( form.handleSubmit(onSubmit), @@ -177,7 +172,9 @@ export const StartProfile: CreateProfileStep = ({ data: chainData }) => { className="w-full" colorScheme="success" onClick={handleSubmit} - isLoading={isLoading || isUploadingShown} + isLoading={ + isLoading || isUploadingShown || profilePicUploader.isUploading + } loadingText={uploadLoadingText} disabled={!form.formState.isValid} > From 6822c0e63213c5ae67d92995a7d4fbc9dfdda0e4 Mon Sep 17 00:00:00 2001 From: Dominik Stumpf Date: Fri, 6 Sep 2024 18:52:15 +0200 Subject: [PATCH 3/8] feat: upload farcaster image to IPFS in EditProfile --- .../(onboarding)/_components/StartProfile.tsx | 1 - .../_components/EditProfile/EditProfile.tsx | 3 +-- .../EditProfile/EditProfileDropdown.tsx | 24 +++++++++++++------ 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/src/app/(marketing)/create-profile/(onboarding)/_components/StartProfile.tsx b/src/app/(marketing)/create-profile/(onboarding)/_components/StartProfile.tsx index 26256fc44d..9a1fc23118 100644 --- a/src/app/(marketing)/create-profile/(onboarding)/_components/StartProfile.tsx +++ b/src/app/(marketing)/create-profile/(onboarding)/_components/StartProfile.tsx @@ -79,7 +79,6 @@ export const StartProfile: CreateProfileStep = ({ data: chainData }) => { farcasterProfile.username ?? form.getValues()?.name ?? "", { shouldValidate: true } ) - void (async function () { if (!farcasterProfile.avatar || isFarcasterAvatarUploaded.current) return const data = await (await fetch(farcasterProfile.avatar)).blob() diff --git a/src/app/(marketing)/profile/_components/EditProfile/EditProfile.tsx b/src/app/(marketing)/profile/_components/EditProfile/EditProfile.tsx index a333ee3fd8..1ad732f88a 100644 --- a/src/app/(marketing)/profile/_components/EditProfile/EditProfile.tsx +++ b/src/app/(marketing)/profile/_components/EditProfile/EditProfile.tsx @@ -11,7 +11,6 @@ import { DialogTitle, DialogTrigger, } from "@/components/ui/Dialog" -import {} from "@/components/ui/DropdownMenu" import { FormControl, FormErrorMessage, @@ -77,7 +76,7 @@ export const EditProfile = ({ children }: PropsWithChildren) => { uploader={profilePicUploader} className="-bottom-2 absolute left-4 translate-y-1/2 bg-muted" /> - + { +export const EditProfileDropdown: FunctionComponent<{ uploader: Uploader }> = ({ + uploader, +}) => { const { farcasterProfiles } = useUser() const farcasterProfile = farcasterProfiles?.at(0) const { setValue } = useFormContext() - const deleteProfile = useDeleteProfile() return ( @@ -38,16 +41,23 @@ export const EditProfileDropdown = () => { { - if (farcasterProfile.avatar) { - setValue("profileImageUrl", farcasterProfile.avatar, { - shouldValidate: true, - }) - } if (farcasterProfile.username) { setValue("name", farcasterProfile.username, { shouldValidate: true, }) } + void (async function () { + if (!farcasterProfile.avatar) return + const data = await (await fetch(farcasterProfile.avatar)).blob() + const fileName = new URL(farcasterProfile.avatar).pathname + .split("/") + .at(-1) + if (!fileName) return + uploader.onUpload({ + data: [new File([data], fileName)], + fileNames: [fileName], + }) + })() }} > Fill data by Farcaster From 299752c90b2fb865b5ddbb1fbaf66cc72e574448 Mon Sep 17 00:00:00 2001 From: valid Date: Fri, 6 Sep 2024 19:36:09 +0200 Subject: [PATCH 4/8] feat(EditProfilePicture): show spinner if progress is not available --- .../_components/EditProfile/EditProfilePicture.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/app/(marketing)/profile/_components/EditProfile/EditProfilePicture.tsx b/src/app/(marketing)/profile/_components/EditProfile/EditProfilePicture.tsx index beb18a1cd1..1f7d08d2e3 100644 --- a/src/app/(marketing)/profile/_components/EditProfile/EditProfilePicture.tsx +++ b/src/app/(marketing)/profile/_components/EditProfile/EditProfilePicture.tsx @@ -5,7 +5,7 @@ import { Button } from "@/components/ui/Button" import { FormField } from "@/components/ui/Form" import { toast } from "@/components/ui/hooks/useToast" import { cn } from "@/lib/utils" -import { Image, UploadSimple, User } from "@phosphor-icons/react" +import { Image, Spinner, UploadSimple, User } from "@phosphor-icons/react" import { AvatarImage } from "@radix-ui/react-avatar" import useDropzone from "hooks/useDropzone" import { Uploader } from "hooks/usePinata/usePinata" @@ -74,7 +74,11 @@ export const EditProfilePicture = ({ )} > {isUploading ? ( -

{(uploadProgress * 100).toFixed(0)}%

+ uploadProgress ? ( +

{(uploadProgress * 100).toFixed(0)}%

+ ) : ( + + ) ) : isDragActive ? ( ) : ( From 38f7b7dd756d33d94b37c6b8c6b66ab38a39168f Mon Sep 17 00:00:00 2001 From: Dominik Stumpf Date: Fri, 6 Sep 2024 19:42:05 +0200 Subject: [PATCH 5/8] refactor: extract image uploader --- .../(onboarding)/_components/StartProfile.tsx | 20 ++++++++----------- .../EditProfile/EditProfileDropdown.tsx | 18 ++++++----------- src/v2/lib/uploadImageUrlToPinata.ts | 13 ++++++++++++ 3 files changed, 27 insertions(+), 24 deletions(-) create mode 100644 src/v2/lib/uploadImageUrlToPinata.ts diff --git a/src/app/(marketing)/create-profile/(onboarding)/_components/StartProfile.tsx b/src/app/(marketing)/create-profile/(onboarding)/_components/StartProfile.tsx index 0ca10a9d6a..cd798bdcee 100644 --- a/src/app/(marketing)/create-profile/(onboarding)/_components/StartProfile.tsx +++ b/src/app/(marketing)/create-profile/(onboarding)/_components/StartProfile.tsx @@ -11,6 +11,7 @@ import { FormLabel, } from "@/components/ui/Form" import { Input } from "@/components/ui/Input" +import { uploadImageUrlAvatarToPinata } from "@/lib/uploadImageUrlToPinata" import { EditProfilePicture } from "@app/(marketing)/profile/_components/EditProfile/EditProfilePicture" import { Schemas, schemas } from "@guildxyz/types" import { zodResolver } from "@hookform/resolvers/zod" @@ -79,18 +80,13 @@ export const StartProfile: CreateProfileStep = ({ data: chainData }) => { farcasterProfile.username ?? form.getValues()?.name ?? "", { shouldValidate: true } ) - void (async function () { - if (!farcasterProfile.avatar || isFarcasterAvatarUploaded.current) return - const data = await (await fetch(farcasterProfile.avatar)).blob() - const fileName = new URL(farcasterProfile.avatar).pathname.split("/").at(-1) - if (!fileName) return - isFarcasterAvatarUploaded.current = true - profilePicUploader.onUpload({ - data: [new File([data], fileName)], - fileNames: [fileName], - }) - })() - }, [farcasterProfile, profilePicUploader.onUpload, form.setValue, form.getValues]) + if (!farcasterProfile.avatar || isFarcasterAvatarUploaded.current) return + uploadImageUrlAvatarToPinata({ + uploader: profilePicUploader, + image: new URL(farcasterProfile.avatar), + }) + isFarcasterAvatarUploaded.current = true + }, [farcasterProfile, profilePicUploader, form.setValue, form.getValues]) const { handleSubmit, isUploadingShown, uploadLoadingText } = useSubmitWithUpload( form.handleSubmit(onSubmit), diff --git a/src/app/(marketing)/profile/_components/EditProfile/EditProfileDropdown.tsx b/src/app/(marketing)/profile/_components/EditProfile/EditProfileDropdown.tsx index 4f70d3b7c4..4ed0ce6605 100644 --- a/src/app/(marketing)/profile/_components/EditProfile/EditProfileDropdown.tsx +++ b/src/app/(marketing)/profile/_components/EditProfile/EditProfileDropdown.tsx @@ -6,6 +6,7 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/DropdownMenu" +import { uploadImageUrlAvatarToPinata } from "@/lib/uploadImageUrlToPinata" import { ArrowsClockwise, DotsThreeVertical, @@ -46,18 +47,11 @@ export const EditProfileDropdown: FunctionComponent<{ uploader: Uploader }> = ({ shouldValidate: true, }) } - void (async function () { - if (!farcasterProfile.avatar) return - const data = await (await fetch(farcasterProfile.avatar)).blob() - const fileName = new URL(farcasterProfile.avatar).pathname - .split("/") - .at(-1) - if (!fileName) return - uploader.onUpload({ - data: [new File([data], fileName)], - fileNames: [fileName], - }) - })() + if (!farcasterProfile.avatar) return + uploadImageUrlAvatarToPinata({ + uploader, + image: new URL(farcasterProfile.avatar), + }) }} > Fill data by Farcaster diff --git a/src/v2/lib/uploadImageUrlToPinata.ts b/src/v2/lib/uploadImageUrlToPinata.ts new file mode 100644 index 0000000000..43b0438577 --- /dev/null +++ b/src/v2/lib/uploadImageUrlToPinata.ts @@ -0,0 +1,13 @@ +import { Uploader } from "hooks/usePinata/usePinata" + +export const uploadImageUrlAvatarToPinata = async ({ + image, + uploader, +}: { image: URL; uploader: Uploader }) => { + const data = await (await fetch(image)).blob() + const fileName = image.pathname.split("/").at(-1) || "unknown" + uploader.onUpload({ + data: [new File([data], fileName)], + fileNames: [fileName], + }) +} From 2286bb0e6883b376b949aa3f785c080562bd4748 Mon Sep 17 00:00:00 2001 From: Dominik Stumpf Date: Fri, 6 Sep 2024 19:46:54 +0200 Subject: [PATCH 6/8] refactor: exclude not required fields --- .../(onboarding)/_components/StartProfile.tsx | 4 ++-- .../profile/_components/EditProfile/EditProfileDropdown.tsx | 2 +- src/v2/lib/uploadImageUrlToPinata.ts | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/app/(marketing)/create-profile/(onboarding)/_components/StartProfile.tsx b/src/app/(marketing)/create-profile/(onboarding)/_components/StartProfile.tsx index cd798bdcee..29ad3ea1b0 100644 --- a/src/app/(marketing)/create-profile/(onboarding)/_components/StartProfile.tsx +++ b/src/app/(marketing)/create-profile/(onboarding)/_components/StartProfile.tsx @@ -82,11 +82,11 @@ export const StartProfile: CreateProfileStep = ({ data: chainData }) => { ) if (!farcasterProfile.avatar || isFarcasterAvatarUploaded.current) return uploadImageUrlAvatarToPinata({ - uploader: profilePicUploader, + onUpload: profilePicUploader.onUpload, image: new URL(farcasterProfile.avatar), }) isFarcasterAvatarUploaded.current = true - }, [farcasterProfile, profilePicUploader, form.setValue, form.getValues]) + }, [farcasterProfile, profilePicUploader.onUpload, form.setValue, form.getValues]) const { handleSubmit, isUploadingShown, uploadLoadingText } = useSubmitWithUpload( form.handleSubmit(onSubmit), diff --git a/src/app/(marketing)/profile/_components/EditProfile/EditProfileDropdown.tsx b/src/app/(marketing)/profile/_components/EditProfile/EditProfileDropdown.tsx index 4ed0ce6605..d9be2b61b5 100644 --- a/src/app/(marketing)/profile/_components/EditProfile/EditProfileDropdown.tsx +++ b/src/app/(marketing)/profile/_components/EditProfile/EditProfileDropdown.tsx @@ -49,7 +49,7 @@ export const EditProfileDropdown: FunctionComponent<{ uploader: Uploader }> = ({ } if (!farcasterProfile.avatar) return uploadImageUrlAvatarToPinata({ - uploader, + onUpload: uploader.onUpload, image: new URL(farcasterProfile.avatar), }) }} diff --git a/src/v2/lib/uploadImageUrlToPinata.ts b/src/v2/lib/uploadImageUrlToPinata.ts index 43b0438577..ce8d511dd1 100644 --- a/src/v2/lib/uploadImageUrlToPinata.ts +++ b/src/v2/lib/uploadImageUrlToPinata.ts @@ -2,11 +2,11 @@ import { Uploader } from "hooks/usePinata/usePinata" export const uploadImageUrlAvatarToPinata = async ({ image, - uploader, -}: { image: URL; uploader: Uploader }) => { + onUpload, +}: { image: URL; onUpload: Uploader["onUpload"] }) => { const data = await (await fetch(image)).blob() const fileName = image.pathname.split("/").at(-1) || "unknown" - uploader.onUpload({ + onUpload({ data: [new File([data], fileName)], fileNames: [fileName], }) From 6721d182cc61c45dc8e6dcbed74b243ba293f1f5 Mon Sep 17 00:00:00 2001 From: Dominik Stumpf Date: Fri, 6 Sep 2024 19:49:14 +0200 Subject: [PATCH 7/8] chore: rename uploader --- .../create-profile/(onboarding)/_components/StartProfile.tsx | 4 ++-- .../profile/_components/EditProfile/EditProfileDropdown.tsx | 4 ++-- src/v2/lib/uploadImageUrlToPinata.ts | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/app/(marketing)/create-profile/(onboarding)/_components/StartProfile.tsx b/src/app/(marketing)/create-profile/(onboarding)/_components/StartProfile.tsx index 29ad3ea1b0..03c5f5d406 100644 --- a/src/app/(marketing)/create-profile/(onboarding)/_components/StartProfile.tsx +++ b/src/app/(marketing)/create-profile/(onboarding)/_components/StartProfile.tsx @@ -11,7 +11,7 @@ import { FormLabel, } from "@/components/ui/Form" import { Input } from "@/components/ui/Input" -import { uploadImageUrlAvatarToPinata } from "@/lib/uploadImageUrlToPinata" +import { uploadImageUrlToPinata } from "@/lib/uploadImageUrlToPinata" import { EditProfilePicture } from "@app/(marketing)/profile/_components/EditProfile/EditProfilePicture" import { Schemas, schemas } from "@guildxyz/types" import { zodResolver } from "@hookform/resolvers/zod" @@ -81,7 +81,7 @@ export const StartProfile: CreateProfileStep = ({ data: chainData }) => { { shouldValidate: true } ) if (!farcasterProfile.avatar || isFarcasterAvatarUploaded.current) return - uploadImageUrlAvatarToPinata({ + uploadImageUrlToPinata({ onUpload: profilePicUploader.onUpload, image: new URL(farcasterProfile.avatar), }) diff --git a/src/app/(marketing)/profile/_components/EditProfile/EditProfileDropdown.tsx b/src/app/(marketing)/profile/_components/EditProfile/EditProfileDropdown.tsx index d9be2b61b5..fe47758315 100644 --- a/src/app/(marketing)/profile/_components/EditProfile/EditProfileDropdown.tsx +++ b/src/app/(marketing)/profile/_components/EditProfile/EditProfileDropdown.tsx @@ -6,7 +6,7 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/DropdownMenu" -import { uploadImageUrlAvatarToPinata } from "@/lib/uploadImageUrlToPinata" +import { uploadImageUrlToPinata } from "@/lib/uploadImageUrlToPinata" import { ArrowsClockwise, DotsThreeVertical, @@ -48,7 +48,7 @@ export const EditProfileDropdown: FunctionComponent<{ uploader: Uploader }> = ({ }) } if (!farcasterProfile.avatar) return - uploadImageUrlAvatarToPinata({ + uploadImageUrlToPinata({ onUpload: uploader.onUpload, image: new URL(farcasterProfile.avatar), }) diff --git a/src/v2/lib/uploadImageUrlToPinata.ts b/src/v2/lib/uploadImageUrlToPinata.ts index ce8d511dd1..cf59940e31 100644 --- a/src/v2/lib/uploadImageUrlToPinata.ts +++ b/src/v2/lib/uploadImageUrlToPinata.ts @@ -1,6 +1,6 @@ import { Uploader } from "hooks/usePinata/usePinata" -export const uploadImageUrlAvatarToPinata = async ({ +export const uploadImageUrlToPinata = async ({ image, onUpload, }: { image: URL; onUpload: Uploader["onUpload"] }) => { From 9a31f53462567f9b80d24b98b4d8db4a0e80e19a Mon Sep 17 00:00:00 2001 From: valid Date: Fri, 6 Sep 2024 19:50:08 +0200 Subject: [PATCH 8/8] cleanup(StartProfile): remove unnecessary loading condition isUploadingShown already handles it, avoiding confusion about why the button might be disabled --- .../create-profile/(onboarding)/_components/StartProfile.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/app/(marketing)/create-profile/(onboarding)/_components/StartProfile.tsx b/src/app/(marketing)/create-profile/(onboarding)/_components/StartProfile.tsx index 0ca10a9d6a..2645efb48d 100644 --- a/src/app/(marketing)/create-profile/(onboarding)/_components/StartProfile.tsx +++ b/src/app/(marketing)/create-profile/(onboarding)/_components/StartProfile.tsx @@ -169,9 +169,7 @@ export const StartProfile: CreateProfileStep = ({ data: chainData }) => { className="w-full" colorScheme="success" onClick={handleSubmit} - isLoading={ - isLoading || isUploadingShown || profilePicUploader.isUploading - } + isLoading={isLoading || isUploadingShown} loadingText={uploadLoadingText} disabled={!form.formState.isValid} >