Hey is a decentralized and permissionless social media app built with Lens Protocol 馃尶
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;