-
-
Notifications
You must be signed in to change notification settings - Fork 173
Add avatar to the onboarding flow #1113
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
db7aed6
9c76adf
39a806e
d20c289
519335d
bf43ddd
4319a8d
6b4eaaa
b82608e
3f4549a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -9,7 +9,6 @@ import { toast } from "sonner"; | |||||||||||||||||
| import type { saveSettingsInput } from "@/schema/profile"; | ||||||||||||||||||
| import { saveSettingsSchema } from "@/schema/profile"; | ||||||||||||||||||
|
|
||||||||||||||||||
| import { uploadFile } from "@/utils/s3helpers"; | ||||||||||||||||||
| import type { user } from "@/server/db/schema"; | ||||||||||||||||||
| import { Button } from "@/components/ui-components/button"; | ||||||||||||||||||
| import { Loader2 } from "lucide-react"; | ||||||||||||||||||
|
|
@@ -25,6 +24,7 @@ import { Textarea } from "@/components/ui-components/textarea"; | |||||||||||||||||
| import { Switch } from "@/components/ui-components/switch"; | ||||||||||||||||||
| import { Divider } from "@/components/ui-components/divider"; | ||||||||||||||||||
| import { Text } from "@/components/ui-components/text"; | ||||||||||||||||||
| import { imageUploadToUrl } from "@/utils/fileUpload"; | ||||||||||||||||||
|
|
||||||||||||||||||
| type User = Pick< | ||||||||||||||||||
| typeof user.$inferSelect, | ||||||||||||||||||
|
|
@@ -75,8 +75,8 @@ const Settings = ({ profile }: { profile: User }) => { | |||||||||||||||||
| }); | ||||||||||||||||||
|
|
||||||||||||||||||
| const { mutate, isError, isSuccess } = api.profile.edit.useMutation(); | ||||||||||||||||||
| const { mutate: getUploadUrl } = api.profile.getUploadUrl.useMutation(); | ||||||||||||||||||
| const { mutate: updateUserPhotoUrl } = | ||||||||||||||||||
| const { mutateAsync: getUploadUrl } = api.profile.getUploadUrl.useMutation(); | ||||||||||||||||||
| const { mutateAsync: updateUserPhotoUrl } = | ||||||||||||||||||
| api.profile.updateProfilePhotoUrl.useMutation(); | ||||||||||||||||||
| const { mutate: updateEmail } = api.profile.updateEmail.useMutation(); | ||||||||||||||||||
|
Comment on lines
+79
to
81
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Codebase verification Action Required: Update The
🔗 Analysis chainLGTM: Mutation hook updated to use The Let's verify if similar changes have been applied to other mutation hooks in the file: Let's attempt to verify the usage of Let's attempt to verify the usage of Let's attempt to verify the usage of Let's attempt to verify if there are other instances of 🏁 Scripts executedThe following scripts were executed for the analysis: Script: #!/bin/bash
# Check if other mutation hooks have been updated to use mutateAsync
rg --type typescript 'const \{ mutate(Async)?: \w+ \} = api\.\w+\.\w+\.useMutation\(\);' app/\(app\)/settings/_client.tsx
Length of output: 158 Script: #!/bin/bash
# Check if other mutation hooks have been updated to use mutateAsync
rg --type tsx 'const \{ mutateAsync: \w+ \} = api\.\w+\.\w+\.useMutation\(\);' app/(app)/settings/_client.tsx
Length of output: 246 Script: #!/bin/bash
# Check if other mutation hooks have been updated to use mutateAsync
rg --type tsx 'const \{ mutateAsync: \w+ \} = api\.\w+\.\w+\.useMutation\(\);' 'app/\(app\)/settings/_client.tsx'
Length of output: 143 Script: #!/bin/bash
# Check if other mutation hooks have been updated to use mutateAsync
rg -g "*.tsx" 'const \{ mutateAsync: \w+ \} = api\.\w+\.\w+\.useMutation\(\);' 'app/(app)/settings/_client.tsx'
Length of output: 113 Script: #!/bin/bash
# Search for any usage of mutateAsync in mutation hooks within the specified file
rg -F 'mutateAsync' 'app/(app)/settings/_client.tsx'
Length of output: 98
Comment on lines
+78
to
81
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Update remaining mutation hook to use Great job updating the However, the -const { mutate: updateEmail } = api.profile.updateEmail.useMutation();
+const { mutateAsync: updateEmail } = api.profile.updateEmail.useMutation();Don't forget to refactor the 📝 Committable suggestion
Suggested change
|
||||||||||||||||||
|
|
||||||||||||||||||
|
|
@@ -104,52 +104,23 @@ const Settings = ({ profile }: { profile: User }) => { | |||||||||||||||||
| mutate({ ...values, newsletter: weeklyNewsletter, emailNotifications }); | ||||||||||||||||||
| }; | ||||||||||||||||||
|
|
||||||||||||||||||
| const uploadToUrl = async (signedUrl: string, file: File) => { | ||||||||||||||||||
| setProfilePhoto({ status: "loading", url: "" }); | ||||||||||||||||||
|
|
||||||||||||||||||
| if (!file) { | ||||||||||||||||||
| setProfilePhoto({ status: "error", url: "" }); | ||||||||||||||||||
| toast.error("Invalid file upload."); | ||||||||||||||||||
| return; | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| const response = await uploadFile(signedUrl, file); | ||||||||||||||||||
| const { fileLocation } = response; | ||||||||||||||||||
| await updateUserPhotoUrl({ | ||||||||||||||||||
| url: fileLocation, | ||||||||||||||||||
| }); | ||||||||||||||||||
|
|
||||||||||||||||||
| return fileLocation; | ||||||||||||||||||
| }; | ||||||||||||||||||
|
|
||||||||||||||||||
| const imageChange = async (e: React.ChangeEvent<HTMLInputElement>) => { | ||||||||||||||||||
| const handleImageChange = async (e: React.ChangeEvent<HTMLInputElement>) => { | ||||||||||||||||||
| if (e.target.files && e.target.files.length > 0) { | ||||||||||||||||||
| const file = e.target.files[0]; | ||||||||||||||||||
| const { size, type } = file; | ||||||||||||||||||
|
|
||||||||||||||||||
| await getUploadUrl( | ||||||||||||||||||
| { size, type }, | ||||||||||||||||||
| { | ||||||||||||||||||
| onError(error) { | ||||||||||||||||||
| if (error) return toast.error(error.message); | ||||||||||||||||||
| return toast.error( | ||||||||||||||||||
| "Something went wrong uploading the photo, please retry.", | ||||||||||||||||||
| ); | ||||||||||||||||||
| }, | ||||||||||||||||||
| async onSuccess(signedUrl) { | ||||||||||||||||||
| const url = await uploadToUrl(signedUrl, file); | ||||||||||||||||||
| if (!url) { | ||||||||||||||||||
| return toast.error( | ||||||||||||||||||
| "Something went wrong uploading the photo, please retry.", | ||||||||||||||||||
| ); | ||||||||||||||||||
| } | ||||||||||||||||||
| setProfilePhoto({ status: "success", url }); | ||||||||||||||||||
| toast.success( | ||||||||||||||||||
| "Profile photo successfully updated. This may take a few minutes to update around the site.", | ||||||||||||||||||
| ); | ||||||||||||||||||
| }, | ||||||||||||||||||
| }, | ||||||||||||||||||
| ); | ||||||||||||||||||
| try { | ||||||||||||||||||
| setProfilePhoto({ status: "loading", url: "" }); | ||||||||||||||||||
| const file = e.target.files[0]; | ||||||||||||||||||
| const { status, fileLocation } = await imageUploadToUrl({ | ||||||||||||||||||
| file, | ||||||||||||||||||
| updateUserPhotoUrl, | ||||||||||||||||||
| getUploadUrl, | ||||||||||||||||||
| }); | ||||||||||||||||||
| setProfilePhoto({ status: status, url: fileLocation }); | ||||||||||||||||||
| } catch (error) { | ||||||||||||||||||
| toast.error("Failed to upload profile photo. Please try again."); | ||||||||||||||||||
| setProfilePhoto({ status: "error", url: "" }); | ||||||||||||||||||
| } | ||||||||||||||||||
| } else { | ||||||||||||||||||
| toast.error("Failed to upload profile photo. Please try again."); | ||||||||||||||||||
| } | ||||||||||||||||||
| }; | ||||||||||||||||||
|
|
||||||||||||||||||
|
|
@@ -225,12 +196,12 @@ const Settings = ({ profile }: { profile: User }) => { | |||||||||||||||||
| id="file-input" | ||||||||||||||||||
| name="user-photo" | ||||||||||||||||||
| accept="image/png, image/gif, image/jpeg" | ||||||||||||||||||
| onChange={imageChange} | ||||||||||||||||||
| onChange={handleImageChange} | ||||||||||||||||||
| className="hidden" | ||||||||||||||||||
| ref={fileInputRef} | ||||||||||||||||||
| /> | ||||||||||||||||||
| <Text className="mt-1 text-xs text-gray-500"> | ||||||||||||||||||
| JPG, GIF or PNG. 1MB max. | ||||||||||||||||||
| JPG, GIF or PNG. 10MB max. | ||||||||||||||||||
| </Text> | ||||||||||||||||||
| </div> | ||||||||||||||||||
| </div> | ||||||||||||||||||
|
|
||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| import { toast } from "sonner"; | ||
| import { uploadFile } from "./s3helpers"; | ||
|
|
||
| type UploadToUrlProps = { | ||
| file: File; | ||
| getUploadUrl: (data: { size: number; type: string }) => Promise<string>; | ||
| updateUserPhotoUrl: (data: { | ||
| url: string; | ||
| }) => Promise<{ role: string; id: string; name: string }>; | ||
| }; | ||
|
|
||
| type UploadResult = { | ||
| status: "success" | "error" | "loading"; | ||
| fileLocation: string; | ||
| }; | ||
|
|
||
| export const imageUploadToUrl = async ({ | ||
| file, | ||
| updateUserPhotoUrl, | ||
| getUploadUrl, | ||
| }: UploadToUrlProps): Promise<UploadResult> => { | ||
| if (!file) { | ||
| toast.error("Invalid file upload."); | ||
| return { status: "error", fileLocation: "" }; | ||
| } | ||
|
Comment on lines
+17
to
+25
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Enhance initial file validation While the initial file check is a good start, consider implementing more comprehensive validation as suggested in the previous review:
Example implementation: if (!file) {
toast.error("No file selected for upload.");
return { status: "error", fileLocation: "" };
}
const allowedTypes = ["image/jpeg", "image/png"];
const maxSize = 5 * 1024 * 1024; // 5 MB
if (!allowedTypes.includes(file.type)) {
toast.error("Invalid file type. Please upload a JPEG or PNG image.");
return { status: "error", fileLocation: "" };
}
if (file.size > maxSize) {
toast.error("File size exceeds the 5MB limit. Please upload a smaller file.");
return { status: "error", fileLocation: "" };
}This will improve error handling and prevent potential issues with unsupported file types or oversized files. |
||
|
|
||
| try { | ||
| const { size, type } = file; | ||
| const signedUrl = await getUploadUrl({ size, type }); | ||
|
|
||
| if (!signedUrl) { | ||
| toast.error("Failed to upload profile photo. Please try again."); | ||
| return { status: "error", fileLocation: "" }; | ||
| } | ||
|
|
||
| const response = await uploadFile(signedUrl, file); | ||
|
|
||
| const { fileLocation } = response; | ||
|
|
||
| if (!fileLocation) { | ||
| toast.error("Failed to retrieve file location after upload."); | ||
| return { status: "error", fileLocation: "" }; | ||
| } | ||
| await updateUserPhotoUrl({ url: fileLocation }); | ||
| toast.success("Profile photo successfully updated."); | ||
|
|
||
| return { status: "success", fileLocation }; | ||
| } catch (error) { | ||
| if (error instanceof Error) { | ||
| toast.error(error.message); | ||
| } else { | ||
| toast.error("Failed to upload profile photo. Please try again."); | ||
| } | ||
| return { status: "error", fileLocation: "" }; | ||
| } | ||
| }; | ||
|
Comment on lines
+48
to
+56
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Enhance error logging for debugging The error handling in the catch block is good, differentiating between Error instances and unknown errors. To further improve debugging capabilities, consider adding error logging: } catch (error) {
console.error('Image upload failed:', error);
if (error instanceof Error) {
toast.error(error.message);
} else {
toast.error("Failed to upload profile photo. Please try again.");
}
return { status: "error", fileLocation: "" };
}This will help with identifying and resolving issues in production environments while maintaining the user-friendly error messages. |
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Add loading indicator for avatar upload
To improve user experience, consider adding a loading indicator when the avatar is being uploaded. This can be done by utilizing the
profilePhoto.statusstate:<Avatar square src={ profilePhoto.status === "error" || profilePhoto.status === "loading" ? undefined : `${profilePhoto.url}` } alt="Profile photo upload section" - className="h-16 w-16 overflow-hidden rounded-full" + className={`h-16 w-16 overflow-hidden rounded-full ${ + profilePhoto.status === "loading" ? "animate-pulse bg-gray-200" : "" + }`} /> +{profilePhoto.status === "loading" && ( + <div className="absolute inset-0 flex items-center justify-center"> + <span className="loading loading-spinner loading-md"></span> + </div> +)}This will show a pulsing animation and a spinner when the avatar is being uploaded, providing visual feedback to the user.
📝 Committable suggestion