Hey is a decentralized and permissionless social media app built with Lens Protocol 馃尶
at main 213 lines 6.5 kB view raw
1import { MeVariables } from "@hey/data/constants"; 2import { ERRORS } from "@hey/data/errors"; 3import { Regex } from "@hey/data/regex"; 4import { useMeLazyQuery, useSetAccountMetadataMutation } from "@hey/indexer"; 5import type { ApolloClientError } from "@hey/types/errors"; 6import { account as accountMetadata } from "@lens-protocol/metadata"; 7import { useCallback, useState } from "react"; 8import { toast } from "sonner"; 9import { z } from "zod"; 10import AvatarUpload from "@/components/Shared/AvatarUpload"; 11import BackButton from "@/components/Shared/BackButton"; 12import CoverUpload from "@/components/Shared/CoverUpload"; 13import { 14 Button, 15 Card, 16 CardHeader, 17 Form, 18 Input, 19 TextArea, 20 useZodForm 21} from "@/components/Shared/UI"; 22import errorToast from "@/helpers/errorToast"; 23import getAccountAttribute from "@/helpers/getAccountAttribute"; 24import prepareAccountMetadata from "@/helpers/prepareAccountMetadata"; 25import uploadMetadata from "@/helpers/uploadMetadata"; 26import useTransactionLifecycle from "@/hooks/useTransactionLifecycle"; 27import useWaitForTransactionToComplete from "@/hooks/useWaitForTransactionToComplete"; 28import { useAccountStore } from "@/store/persisted/useAccountStore"; 29 30const ValidationSchema = z.object({ 31 bio: z.string().max(260, { message: "Bio should not exceed 260 characters" }), 32 location: z.string().max(100, { 33 message: "Location should not exceed 100 characters" 34 }), 35 name: z 36 .string() 37 .max(100, { message: "Name should not exceed 100 characters" }) 38 .regex(Regex.accountNameValidator, { 39 message: "Account name must not contain restricted symbols" 40 }), 41 website: z.union([ 42 z.string().regex(Regex.url, { message: "Invalid website" }), 43 z.string().max(0) 44 ]), 45 x: z.string().max(100, { message: "X handle must not exceed 100 characters" }) 46}); 47 48const PersonalizeSettingsForm = () => { 49 const { currentAccount, setCurrentAccount } = useAccountStore(); 50 const [isSubmitting, setIsSubmitting] = useState(false); 51 const [avatarUrl, setAvatarUrl] = useState<string | undefined>( 52 currentAccount?.metadata?.picture 53 ); 54 const [coverUrl, setCoverUrl] = useState<string | undefined>( 55 currentAccount?.metadata?.coverPicture 56 ); 57 const handleTransactionLifecycle = useTransactionLifecycle(); 58 const waitForTransactionToComplete = useWaitForTransactionToComplete(); 59 const [getCurrentAccountDetails] = useMeLazyQuery({ 60 fetchPolicy: "no-cache", 61 variables: MeVariables 62 }); 63 64 const onCompleted = async (hash: string) => { 65 await waitForTransactionToComplete(hash); 66 const accountData = await getCurrentAccountDetails(); 67 setCurrentAccount(accountData?.data?.me.loggedInAs.account); 68 setIsSubmitting(false); 69 toast.success("Account updated"); 70 }; 71 72 const onError = useCallback((error: ApolloClientError) => { 73 setIsSubmitting(false); 74 errorToast(error); 75 }, []); 76 77 const [setAccountMetadata] = useSetAccountMetadataMutation({ 78 onCompleted: async ({ setAccountMetadata }) => { 79 if (setAccountMetadata.__typename === "SetAccountMetadataResponse") { 80 return onCompleted(setAccountMetadata.hash); 81 } 82 83 return await handleTransactionLifecycle({ 84 onCompleted, 85 onError, 86 transactionData: setAccountMetadata 87 }); 88 }, 89 onError 90 }); 91 92 const form = useZodForm({ 93 defaultValues: { 94 bio: currentAccount?.metadata?.bio || "", 95 location: getAccountAttribute( 96 "location", 97 currentAccount?.metadata?.attributes 98 ), 99 name: currentAccount?.metadata?.name || "", 100 website: getAccountAttribute( 101 "website", 102 currentAccount?.metadata?.attributes 103 ), 104 x: getAccountAttribute( 105 "x", 106 currentAccount?.metadata?.attributes 107 )?.replace(/(https:\/\/)?x\.com\//, "") 108 }, 109 schema: ValidationSchema 110 }); 111 112 const updateAccount = async ( 113 data: z.infer<typeof ValidationSchema>, 114 avatarUrl: string | undefined, 115 coverUrl: string | undefined 116 ) => { 117 if (!currentAccount) { 118 return toast.error(ERRORS.SignWallet); 119 } 120 121 setIsSubmitting(true); 122 const preparedAccountMetadata = prepareAccountMetadata(currentAccount, { 123 attributes: { 124 location: data.location, 125 website: data.website, 126 x: data.x 127 }, 128 bio: data.bio, 129 coverPicture: coverUrl, 130 name: data.name, 131 picture: avatarUrl 132 }); 133 const metadataUri = await uploadMetadata( 134 accountMetadata(preparedAccountMetadata) 135 ); 136 137 return await setAccountMetadata({ 138 variables: { request: { metadataUri } } 139 }); 140 }; 141 142 const onSetAvatar = async (src: string | undefined) => { 143 setAvatarUrl(src); 144 return await updateAccount({ ...form.getValues() }, src, coverUrl); 145 }; 146 147 const onSetCover = async (src: string | undefined) => { 148 setCoverUrl(src); 149 return await updateAccount({ ...form.getValues() }, avatarUrl, src); 150 }; 151 152 return ( 153 <Card> 154 <CardHeader icon={<BackButton path="/settings" />} title="Personalize" /> 155 <Form 156 className="space-y-4 p-5" 157 form={form} 158 onSubmit={(data) => updateAccount(data, avatarUrl, coverUrl)} 159 > 160 <Input 161 disabled 162 label="Account Address" 163 type="text" 164 value={currentAccount?.address} 165 /> 166 <Input 167 label="Name" 168 placeholder="Gavin" 169 type="text" 170 {...form.register("name")} 171 /> 172 <Input 173 label="Location" 174 placeholder="Miami" 175 type="text" 176 {...form.register("location")} 177 /> 178 <Input 179 label="Website" 180 placeholder="https://hooli.com" 181 type="text" 182 {...form.register("website")} 183 /> 184 <Input 185 label="X" 186 placeholder="gavin" 187 prefix="https://x.com" 188 type="text" 189 {...form.register("x")} 190 /> 191 <TextArea 192 label="Bio" 193 placeholder="Tell us something about you!" 194 {...form.register("bio")} 195 /> 196 <AvatarUpload setSrc={onSetAvatar} src={avatarUrl || ""} /> 197 <CoverUpload setSrc={onSetCover} src={coverUrl || ""} /> 198 <Button 199 className="ml-auto" 200 disabled={ 201 isSubmitting || (!form.formState.isDirty && !coverUrl && !avatarUrl) 202 } 203 loading={isSubmitting} 204 type="submit" 205 > 206 Save 207 </Button> 208 </Form> 209 </Card> 210 ); 211}; 212 213export default PersonalizeSettingsForm;