Skip to content

Conversation

@sahilpatel2712
Copy link
Contributor

✨ Codu Pull Request 💻

Fixes #1008

Pull Request details

  • Add an avatar to the onboarding flow
  • Avatar can be changed by the user

Any Breaking changes

'None'

Associated Screenshots

image

@sahilpatel2712 sahilpatel2712 requested a review from a team as a code owner October 13, 2024 09:50
@vercel
Copy link

vercel bot commented Oct 13, 2024

@sahilpatel2712 is attempting to deploy a commit to the Codú Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 13, 2024

Walkthrough

The pull request introduces enhancements to the AdditionalSignUpDetails component, allowing users to upload and manage their profile pictures during the sign-up process. Key updates include new types for user details, methods for handling file uploads, and modifications to the Page function to retrieve and display the user's image. The changes aim to streamline user profile management by integrating an avatar feature into the onboarding flow.

Changes

File Path Change Summary
app/(app)/alpha/additional-details/_client.tsx - Enhanced AdditionalSignUpDetails component with profile picture upload functionality.
- Added image property to UserDetails type.
- Introduced ProfilePhoto type.
- Implemented handleImageChange, getUploadUrl, and updateUserPhotoUrl methods for handling uploads.
app/(app)/alpha/additional-details/page.tsx - Modified Page function to include image field in user retrieval and ensure it is passed to the Content component.
app/(app)/settings/_client.tsx - Updated Settings component to use imageUploadToUrl for profile photo uploads with improved error handling and state management.
utils/fileUpload.ts - Introduced imageUploadToUrl function for file uploads, along with types for upload parameters and results.

Assessment against linked issues

Objective Addressed Explanation
Add avatar to the onboarding flow (#1008)

Possibly related PRs

  • Refactor settings page to include catalyst components #1108: The changes in the main PR regarding the Avatar component and profile photo upload are related to the modifications in the Settings component, which also includes an Avatar for displaying the profile picture and file selection for uploads.

🐇 In the meadow where bunnies play,
A new avatar brightens the day.
With a click and a smile, users can share,
Their profile pictures, a joy to declare!
Hopping along, we embrace this new feature,
A delightful upgrade, oh what a pleasure! 🌼


Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Outside diff range and nitpick comments (6)
app/(app)/alpha/additional-details/page.tsx (2)

41-41: LGTM: Image field added to detailsWithNullsRemoved object

The addition of the image field to the detailsWithNullsRemoved object is correct and consistent with the handling of other fields. This ensures that the user's image is passed to the Content component, which is crucial for displaying the avatar in the onboarding flow.

For consistency with TypeScript best practices, consider using nullish coalescing (??) instead of logical OR (||) for the default value. This would handle cases where an empty string is a valid value:

image: details?.image ?? "",

This suggestion is optional and doesn't affect the functionality of the current implementation.


Line range hint 1-44: Overall implementation looks good, consider additional enhancements

The changes successfully implement the retrieval and passing of the user's image to the Content component, aligning well with the PR objective of adding an avatar to the onboarding flow. The implementation is consistent with the existing code structure and error handling approach.

To further enhance the feature, consider the following suggestions:

  1. Ensure that the Content component is updated to display the avatar using the newly passed image field.
  2. If not already implemented elsewhere, consider adding functionality for image upload during the onboarding process.
  3. Add error handling for cases where the image retrieval fails or the image is in an unsupported format.
app/(app)/alpha/additional-details/_client.tsx (4)

152-157: Simplify the onError handler in mutation

The onError handler always receives an error object, so the check if (error) is unnecessary. Simplify the code by removing this conditional.

Here's the simplified code:

 onError(error) {
-  if (error) return toast.error(error.message);
-  return toast.error(
-    "Something went wrong uploading the photo, please retry.",
-  );
+  toast.error(error.message);
 },

This makes the error handling clearer and more concise.


210-210: Remove unnecessary template literal in Avatar component

Since profilePhoto.url is already a string, you can pass it directly to the src prop without using a template literal.

Apply this diff to simplify the code:

             ? undefined
-            : `${profilePhoto.url}`
+            : profilePhoto.url

This avoids unnecessary string interpolation.


Line range hint 265-267: Adjust progress calculation to reflect the correct number of slides

The SignupProgressBar component calculates progress based on (100 / 4) * currentSlide, but there are only 3 slides in the onboarding process. This causes the progress to never reach 100%. Adjust the calculation to use the correct number of slides.

Here's how you can fix it:

 function SignupProgressBar({ currentSlide }: { currentSlide: number }) {
-  const progressPercentage = (100 / 4) * currentSlide;
+  const progressPercentage = (100 / 3) * currentSlide;

   return (
     <div className="flex h-2">

This change ensures the progress bar accurately reflects the user's progress.


Line range hint 296-316: Fix logic inversion in form validation for SlideThree

In the onFormSubmit function, the trigger method returns true when the validation passes, but the variable isError suggests the opposite. This inversion can lead to unintended behavior during form submission.

Here's how you can correct the logic:

 const onFormSubmit = async (data: TypeSlideThreeSchema) => {
-  let isError = await trigger(["professionalOrStudent"]);
+  let isValid = await trigger(["professionalOrStudent"]);
   const professionalOrStudent = getValues("professionalOrStudent");

-  if (isError && professionalOrStudent === "Working professional") {
-    isError = await trigger(["workplace", "jobTitle"]);
+  if (isValid && professionalOrStudent === "Working professional") {
+    isValid = await trigger(["workplace", "jobTitle"]);
   }

-  if (isError && professionalOrStudent === "Current student") {
-    isError = await trigger(["levelOfStudy", "course"]);
+  if (isValid && professionalOrStudent === "Current student") {
+    isValid = await trigger(["levelOfStudy", "course"]);
   }

-  if (isError) {
+  if (isValid) {
     try {
       const isSuccess = await slideThreeSubmitAction(data);
       if (isSuccess) {
         toast.success("Saved");
         router.push(`/`, { scroll: false });
       } else {
         toast.error("Error, saving was unsuccessful.");
       }
     } catch (error) {
       toast.error("An unexpected error occurred.");
     }
   }
 };

This ensures that the form submission only proceeds when validation passes.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Files that changed from the base of the PR and between d45d070 and db7aed6.

📒 Files selected for processing (2)
  • app/(app)/alpha/additional-details/_client.tsx (6 hunks)
  • app/(app)/alpha/additional-details/page.tsx (2 hunks)
🧰 Additional context used
🔇 Additional comments (1)
app/(app)/alpha/additional-details/page.tsx (1)

25-25: LGTM: Image field added to database query

The addition of the image: true field to the columns object in the database query is correct and aligns well with the PR objective of adding an avatar to the onboarding flow. This change enables the retrieval of the user's image from the database, which is essential for displaying the avatar in the onboarding process.

Comment on lines 116 to 118
const { mutate: getUploadUrl } = api.profile.getUploadUrl.useMutation();
const { mutate: updateUserPhotoUrl } =
api.profile.updateProfilePhotoUrl.useMutation();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Use mutateAsync instead of mutate when awaiting mutation results

To properly await the results of mutations and handle potential errors, use mutateAsync instead of mutate. This ensures that you can await the promise returned by the mutation functions.

Apply this diff to fix the issue:

- const { mutate: getUploadUrl } = api.profile.getUploadUrl.useMutation();
- const { mutate: updateUserPhotoUrl } =
-   api.profile.updateProfilePhotoUrl.useMutation();

+ const { mutateAsync: getUploadUrl } = api.profile.getUploadUrl.useMutation();
+ const { mutateAsync: updateUserPhotoUrl } =
+   api.profile.updateProfilePhotoUrl.useMutation();

This change allows you to use await getUploadUrl(...) and await updateUserPhotoUrl(...) as intended.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const { mutate: getUploadUrl } = api.profile.getUploadUrl.useMutation();
const { mutate: updateUserPhotoUrl } =
api.profile.updateProfilePhotoUrl.useMutation();
const { mutateAsync: getUploadUrl } = api.profile.getUploadUrl.useMutation();
const { mutateAsync: updateUserPhotoUrl } =
api.profile.updateProfilePhotoUrl.useMutation();

Comment on lines 145 to 147
if (e.target.files && e.target.files.length > 0) {
const file = e.target.files[0];
const { size, type } = file;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add file size validation to enforce maximum upload size

Currently, there is no validation on the file size. Since the UI indicates a maximum file size of 1MB, consider adding a check to ensure that the uploaded file does not exceed this limit.

Here's how you can add the validation:

 const imageChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
   if (e.target.files && e.target.files.length > 0) {
     const file = e.target.files[0];
     const { size, type } = file;

+    // Check file size (1MB = 1,048,576 bytes)
+    if (size > 1048576) {
+      toast.error("File size exceeds 1MB. Please upload a smaller image.");
+      return;
+    }

     await getUploadUrl(
       { size, type },
       {
         // ...

This ensures users are notified if the selected file exceeds the allowed size.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (e.target.files && e.target.files.length > 0) {
const file = e.target.files[0];
const { size, type } = file;
if (e.target.files && e.target.files.length > 0) {
const file = e.target.files[0];
const { size, type } = file;
// Check file size (1MB = 1,048,576 bytes)
if (size > 1048576) {
toast.error("File size exceeds 1MB. Please upload a smaller image.");
return;
}

⚠️ Potential issue

Add file type validation to ensure accepted image formats

While the accept attribute on the <Input> element filters the file selection dialog, users might bypass this restriction. Add a check to validate the file's MIME type before uploading.

Here's how you can add the validation:

 const imageChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
   if (e.target.files && e.target.files.length > 0) {
     const file = e.target.files[0];
     const { size, type } = file;

+    // Validate file type
+    const acceptedTypes = ["image/png", "image/gif", "image/jpeg"];
+    if (!acceptedTypes.includes(type)) {
+      toast.error("Invalid file type. Please upload a JPG, GIF, or PNG image.");
+      return;
+    }

     // Check file size (1MB = 1,048,576 bytes)
     if (size > 1048576) {
       toast.error("File size exceeds 1MB. Please upload a smaller image.");
       return;
     }

     await getUploadUrl(
       { size, type },
       {
         // ...

This ensures only valid image formats are processed.

Committable suggestion was skipped due to low confidence.

Comment on lines 127 to 143
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;
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Handle errors from uploadFile and updateUserPhotoUrl functions

Currently, the uploadToUrl function does not handle errors that might occur during the file upload or while updating the user's photo URL. To improve robustness, consider adding error handling to these operations.

Here's how you can enhance error handling:

 const uploadToUrl = async (signedUrl: string, file: File) => {
   setProfilePhoto({ status: "loading", url: "" });

   if (!file) {
     setProfilePhoto({ status: "error", url: "" });
     toast.error("Invalid file upload.");
     return;
   }

+  try {
     const response = await uploadFile(signedUrl, file);
     const { fileLocation } = response;
     await updateUserPhotoUrl({
       url: fileLocation,
     });
     return fileLocation;
+  } catch (error) {
+    setProfilePhoto({ status: "error", url: "" });
+    toast.error("Failed to upload profile photo. Please try again.");
+    return null;
+  }
 };

This ensures that any errors during the upload process are properly handled and communicated to the user.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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 uploadToUrl = async (signedUrl: string, file: File) => {
setProfilePhoto({ status: "loading", url: "" });
if (!file) {
setProfilePhoto({ status: "error", url: "" });
toast.error("Invalid file upload.");
return;
}
try {
const response = await uploadFile(signedUrl, file);
const { fileLocation } = response;
await updateUserPhotoUrl({
url: fileLocation,
});
return fileLocation;
} catch (error) {
setProfilePhoto({ status: "error", url: "" });
toast.error("Failed to upload profile photo. Please try again.");
return null;
}
};

@John-Paul-Larkin
Copy link
Member

Hey @sahilpatel2712
Thanks for this PR, however, there are some issues pointed out by code rabbit. Especially around the error handling and file size limit.
I see this code follows the approach used on the existing Settings page, but there’s definitely room to make some of the same improvements there also.

Also, the alignment of the elements you added are not visually balanced. Again, I realise this code comes directly from the existing settings page, however in this case the styles should be customised for this form.
"Profile picture", the avatar and "Full Name" should be aligned for a cleaner look. Similarly the "Change avatar" button should be aligned in a cihesive way with the avatar.

Thanks,
JP

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Outside diff range and nitpick comments (2)
app/(app)/alpha/additional-details/_client.tsx (2)

207-245: Improve alignment of avatar upload section

To enhance the visual balance of the avatar upload section, consider adjusting the alignment of the "Change avatar" button and the instruction text. Here's a suggested improvement:

 <div className="mx-4 my-4">
   <Field className="flex-grow">
     <Label>Profile Picture</Label>
-    <div className="mt-3 flex items-center justify-between gap-4 px-3">
+    <div className="mt-3 flex items-center gap-4 px-3">
       <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"
       />
-      <div className="pt-[30px]">
+      <div className="flex flex-col items-start justify-center">
         <Button
           color="dark/white"
           type="button"
-          className="h-[30px] rounded-md text-xs"
+          className="mb-1 h-[30px] rounded-md text-xs"
           onClick={() => fileInputRef.current?.click()}
         >
           Change avatar
         </Button>
         <Input
           type="file"
           id="file-input"
           name="user-photo"
           accept="image/png, image/gif, image/jpeg"
           onChange={imageChange}
           className="hidden"
           ref={fileInputRef}
         />
-        <Text className="mt-1 text-xs text-gray-500">
+        <Text className="text-xs text-gray-500">
           JPG, GIF or PNG. 10MB max.
         </Text>
       </div>
     </div>
   </Field>
 </div>

These changes will align the "Change avatar" button and instruction text with the avatar, creating a more cohesive look.


Inconsistent File Size Limits Detected

  • The UI text indicates a 1MB maximum file size.
  • Other parts of the codebase enforce 10MB and 20MB limits.

Affected Files:

  • server/api/router/profile.ts: "Maximum file size 10mb"
  • components/editor/editor/plugins/upload-images.tsx: "File size too big (max 20MB)."
🔗 Analysis chain

Line range hint 3-45: Update file size limit in UI text

The UI text mentions a 10MB max file size, but the previous review comments suggest a 1MB limit. Ensure consistency between the implemented limit and the UI text.

-                  JPG, GIF or PNG. 10MB max.
+                  JPG, GIF or PNG. 1MB max.

Also applies to: 58-63


Let's correct the shell script to use the appropriate file type identifiers and re-run the search for file size limits.


Let's adjust the shell script to specify file extensions directly and re-run the search for file size limits.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Check for other occurrences of file size limits
rg -i "file.?size|size.?limit" --type typescript --type javascript

Length of output: 103


Script:

#!/bin/bash
# Check for other occurrences of file size limits with correct file types
rg -i "file.?size|size.?limit" --type ts --type tsx --type js --type jsx

Length of output: 102


Script:

#!/bin/bash
# Check for other occurrences of file size limits using file extensions
rg -i "file.?size|size.?limit" -g "*.ts" -g "*.tsx" -g "*.js" -g "*.jsx"

Length of output: 559

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Files that changed from the base of the PR and between db7aed6 and 39a806e.

📒 Files selected for processing (1)
  • app/(app)/alpha/additional-details/_client.tsx (6 hunks)
🧰 Additional context used

Comment on lines +211 to +218
<Avatar
square
src={
profilePhoto.status === "error" ||
profilePhoto.status === "loading"
? undefined
: `${profilePhoto.url}`
}
Copy link
Contributor

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.status state:

 <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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<Avatar
square
src={
profilePhoto.status === "error" ||
profilePhoto.status === "loading"
? undefined
: `${profilePhoto.url}`
}
<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 ${
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>
)}

Comment on lines 129 to 150
const uploadToUrl = async (signedUrl: string, file: File) => {
setProfilePhoto({ status: "loading", url: "" });

if (!file) {
setProfilePhoto({ status: "error", url: "" });
toast.error("Invalid file upload.");
return;
}
try {
const response = await uploadFile(signedUrl, file);
const { fileLocation } = response;
await updateUserPhotoUrl({
url: fileLocation,
});

return fileLocation;
} catch (error) {
setProfilePhoto({ status: "error", url: "" });
toast.error("Failed to upload profile photo. Please try again.");
return null;
}
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Enhance error handling in uploadToUrl function

Consider adding more specific error handling to provide better feedback to the user. For example:

 try {
   const response = await uploadFile(signedUrl, file);
   const { fileLocation } = response;
   await updateUserPhotoUrl({
     url: fileLocation,
   });

   return fileLocation;
 } catch (error) {
   setProfilePhoto({ status: "error", url: "" });
-  toast.error("Failed to upload profile photo. Please try again.");
+  if (error instanceof Error) {
+    toast.error(`Failed to upload profile photo: ${error.message}`);
+  } else {
+    toast.error("Failed to upload profile photo. Please try again.");
+  }
   return null;
 }

This will provide more specific error messages when possible.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const uploadToUrl = async (signedUrl: string, file: File) => {
setProfilePhoto({ status: "loading", url: "" });
if (!file) {
setProfilePhoto({ status: "error", url: "" });
toast.error("Invalid file upload.");
return;
}
try {
const response = await uploadFile(signedUrl, file);
const { fileLocation } = response;
await updateUserPhotoUrl({
url: fileLocation,
});
return fileLocation;
} catch (error) {
setProfilePhoto({ status: "error", url: "" });
toast.error("Failed to upload profile photo. Please try again.");
return null;
}
};
const uploadToUrl = async (signedUrl: string, file: File) => {
setProfilePhoto({ status: "loading", url: "" });
if (!file) {
setProfilePhoto({ status: "error", url: "" });
toast.error("Invalid file upload.");
return;
}
try {
const response = await uploadFile(signedUrl, file);
const { fileLocation } = response;
await updateUserPhotoUrl({
url: fileLocation,
});
return fileLocation;
} catch (error) {
setProfilePhoto({ status: "error", url: "" });
if (error instanceof Error) {
toast.error(`Failed to upload profile photo: ${error.message}`);
} else {
toast.error("Failed to upload profile photo. Please try again.");
}
return null;
}
};

Comment on lines 151 to 179
const imageChange = 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.",
);
},
},
);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Implement file type and size validation

As suggested in previous comments, add file type and size validation before uploading:

 const imageChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
   if (e.target.files && e.target.files.length > 0) {
     const file = e.target.files[0];
     const { size, type } = file;

+    // Validate file type
+    const acceptedTypes = ["image/png", "image/gif", "image/jpeg"];
+    if (!acceptedTypes.includes(type)) {
+      toast.error("Invalid file type. Please upload a JPG, GIF, or PNG image.");
+      return;
+    }
+
+    // Validate file size (1MB = 1,048,576 bytes)
+    if (size > 1048576) {
+      toast.error("File size exceeds 1MB. Please upload a smaller image.");
+      return;
+    }

     await getUploadUrl(
       { size, type },
       {
         // ... rest of the function

This ensures that only valid file types and sizes are processed, improving user experience and reducing server load.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const imageChange = 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.",
);
},
},
);
}
const imageChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files && e.target.files.length > 0) {
const file = e.target.files[0];
const { size, type } = file;
// Validate file type
const acceptedTypes = ["image/png", "image/gif", "image/jpeg"];
if (!acceptedTypes.includes(type)) {
toast.error("Invalid file type. Please upload a JPG, GIF, or PNG image.");
return;
}
// Validate file size (1MB = 1,048,576 bytes)
if (size > 1048576) {
toast.error("File size exceeds 1MB. Please upload a smaller image.");
return;
}
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.",
);
},
},
);
}

@sahilpatel2712
Copy link
Contributor Author

sahilpatel2712 commented Oct 13, 2024

Hi @John-Paul-Larkin
Thank you for mentioning the avatar alignment issues, I solved those issues in the next commit 9c76adf , and issues pointed by code Rabit
about validate file type and file size are already done in getUploadUrl method located at folder
https://github.com/codu-code/codu/blob/develop/server/api/router/profile.ts
Please let me know if any further changes are needed.

image

@github-actions
Copy link

Uh oh! @sahilpatel2712, the image you shared is missing helpful alt text. Check #1113 (comment).

Alt text is an invisible description that helps screen readers describe images to blind or low-vision users. If you are using markdown to display images, add your alt text inside the brackets of the markdown image.

Learn more about alt text at Basic writing and formatting syntax: images on GitHub Docs.

3 similar comments
@github-actions
Copy link

Uh oh! @sahilpatel2712, the image you shared is missing helpful alt text. Check #1113 (comment).

Alt text is an invisible description that helps screen readers describe images to blind or low-vision users. If you are using markdown to display images, add your alt text inside the brackets of the markdown image.

Learn more about alt text at Basic writing and formatting syntax: images on GitHub Docs.

@github-actions
Copy link

Uh oh! @sahilpatel2712, the image you shared is missing helpful alt text. Check #1113 (comment).

Alt text is an invisible description that helps screen readers describe images to blind or low-vision users. If you are using markdown to display images, add your alt text inside the brackets of the markdown image.

Learn more about alt text at Basic writing and formatting syntax: images on GitHub Docs.

@github-actions
Copy link

Uh oh! @sahilpatel2712, the image you shared is missing helpful alt text. Check #1113 (comment).

Alt text is an invisible description that helps screen readers describe images to blind or low-vision users. If you are using markdown to display images, add your alt text inside the brackets of the markdown image.

Learn more about alt text at Basic writing and formatting syntax: images on GitHub Docs.

Copy link
Member

@John-Paul-Larkin John-Paul-Larkin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @sahilpatel2712

Thank you for implementing the recent changes.

At this point, the uploadToUrl functionality is being used both on the settings page and the additional-details page. However, the code has started to diverge due to the error handling modifications you introduced.

Given this, it would make sense to refactor the logic into a shared utility function. Could you please extract this logic into a utility function and place it in the utils folder so it can be reused in both places?

Thanks!

@sahilpatel2712 sahilpatel2712 marked this pull request as draft October 14, 2024 08:30
@sahilpatel2712 sahilpatel2712 marked this pull request as ready for review October 14, 2024 10:10
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Outside diff range and nitpick comments (4)
app/(app)/alpha/additional-details/_client.tsx (1)

112-120: LGTM: File upload setup in SlideOne function

The changes in the SlideOne function are well-implemented:

  • The fileInputRef is correctly used for the file input.
  • The profilePhoto state is appropriately managed with useState.
  • The tRPC mutations for getting the upload URL and updating the user's photo URL are correctly implemented.

Consider adding error handling for the updateUserPhotoUrl mutation, similar to how you've handled errors for getUploadUrl. This would provide a more consistent error handling approach throughout the component.

utils/fileUpload.ts (3)

12-15: Consider removing the 'loading' status from UploadResult

The status type includes 'loading', but the uploadToUrl function only returns 'success' or 'error'. Removing 'loading' from the status type can simplify the code and avoid confusion.


35-38: Log errors in the catch block for better debugging

Capturing and logging the error can aid in diagnosing issues during the upload process. This information can be invaluable during development and troubleshooting.

For example:

} catch (error) {
  console.error("Error uploading profile photo:", error);
  toast.error("Failed to upload profile photo. Please try again.");
  return { status: "error", fileLocation: null };
}

7-9: Simplify the return type of updateUserPhotoUrl if return values are unused

If the returned values 'role', 'id', and 'name' from updateUserPhotoUrl are not utilized in this context, consider changing the return type to Promise<void> for clarity and simplicity.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Files that changed from the base of the PR and between 39a806e and 519335d.

📒 Files selected for processing (3)
  • app/(app)/alpha/additional-details/_client.tsx (6 hunks)
  • app/(app)/settings/_client.tsx (3 hunks)
  • utils/fileUpload.ts (1 hunks)
🧰 Additional context used
🔇 Additional comments (6)
app/(app)/alpha/additional-details/_client.tsx (2)

3-3: LGTM: New imports and type definitions for profile picture feature

The new imports and type definitions are well-structured and appropriate for implementing the profile picture feature. The UserDetails type has been correctly updated to include the image property, and the new ProfilePhoto type is well-defined for managing the upload state.

Also applies to: 42-45, 58-63


Line range hint 1-729: Overall assessment: Good implementation with room for minor improvements

The implementation of the profile picture upload feature is well-structured and functional. The changes include appropriate imports, type definitions, and UI components. The SlideOne function has been effectively updated to handle file uploads, and the new imageChange function manages the upload process.

However, there are a few areas that could be improved:

  1. Add file type and size validation before upload.
  2. Enhance error handling with more specific error messages.
  3. Improve UI feedback during the upload process (loading states, button disabling).
  4. Ensure consistency between the stated and implemented file size limits.

Addressing these points will result in a more robust and user-friendly implementation. Great work overall!

app/(app)/settings/_client.tsx (4)

27-27: Import statement for uploadToUrl added correctly

The addition of the import statement for uploadToUrl from @/utils/fileUpload is appropriate and necessary for handling file uploads.


112-112: Initializing profile photo upload state

Setting the profile photo status to "loading" and clearing the URL before starting the upload process is appropriate. This provides immediate feedback to the user that the upload is in progress.


125-129: Verify parameters passed to uploadToUrl

Ensure that the uploadToUrl function accepts the parameters { signedUrl, file, updateUserPhotoUrl } and that updateUserPhotoUrl is used correctly within this function. Since updateUserPhotoUrl is now using mutateAsync, confirm that it is awaited appropriately inside uploadToUrl.


131-134: Handling upload success and error states appropriately

The logic for updating the profile photo state based on the result of the upload is correctly implemented. It sets the state to "success" and updates the URL when the upload is successful, and sets it to "error" otherwise.

Comment on lines 22 to 25
if (!file) {
toast.error("Invalid file upload.");
return { status: "error", fileLocation: null };
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add validation for file type and size before uploading

Currently, the code only checks if a file is provided. To enhance security and user experience, consider validating the file's type and size before uploading. This can prevent unsupported file formats and oversized files from causing issues.

For example:

if (!file) {
  toast.error("No file selected for upload.");
  return { status: "error", fileLocation: null };
}

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: null };
}

if (file.size > maxSize) {
  toast.error("File size exceeds the 5MB limit. Please upload a smaller file.");
  return { status: "error", fileLocation: null };
}

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Outside diff range and nitpick comments (1)
app/(app)/settings/_client.tsx (1)

112-134: LGTM: Improved error handling and state management in imageChange

The imageChange function has been significantly improved:

  • The profilePhoto state is now set to "loading" before starting the upload process.
  • Error handling has been enhanced with more specific error messages.
  • The uploadToUrl function is called with the correct parameters.
  • The success case properly sets the profile photo URL based on the upload response.

These changes improve the user experience and error reporting.

Consider adding a more specific error message for the case when status is not "success":

 if (status === "success" && fileLocation) {
   setProfilePhoto({ status: "success", url: fileLocation });
 } else {
-  setProfilePhoto({ status: "error", url: "" });
+  setProfilePhoto({ status: "error", url: "" });
+  toast.error("Failed to update profile photo. Please try again.");
 }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Files that changed from the base of the PR and between 519335d and 4319a8d.

📒 Files selected for processing (3)
  • app/(app)/alpha/additional-details/_client.tsx (6 hunks)
  • app/(app)/settings/_client.tsx (4 hunks)
  • utils/fileUpload.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • utils/fileUpload.ts
🧰 Additional context used
🔇 Additional comments (5)
app/(app)/settings/_client.tsx (2)

27-27: LGTM: Import statement updated correctly

The import statement has been updated to use uploadToUrl from @/utils/fileUpload, which aligns with the changes in the imageChange function.


219-219: Verify server-side file size limit

The UI text has been updated to indicate a maximum file size of 10MB, which is a user-friendly improvement. However, we need to ensure that the server-side validation also supports this new limit.

Let's verify the server-side file size limit:

Please confirm that the server-side validation has been updated to allow files up to 10MB.

app/(app)/alpha/additional-details/_client.tsx (3)

3-3: New imports added for profile picture functionality

The new imports for Avatar, Text, api, and uploadToUrl are correctly added to support the new profile picture upload feature.

Also applies to: 42-45


58-59: New types added for profile picture support

The UserDetails type has been correctly updated to include the image property, and a new ProfilePhoto type has been added to manage the profile photo upload state.

Also applies to: 61-63


Line range hint 1-729: Overall assessment of profile picture upload implementation

The implementation of the profile picture upload feature in the SlideOne component is generally well-done and addresses the main objective of the PR. The changes include:

  1. Adding necessary imports and type definitions
  2. Implementing state management for the profile photo
  3. Adding mutations for URL generation and photo upload
  4. Creating an imageChange function to handle the upload process
  5. Updating the UI to include the avatar and upload functionality

However, there are some areas for improvement:

  1. File type and size validation should be added before initiating the upload
  2. Error handling can be enhanced to provide more specific feedback
  3. UI can be improved to better handle loading states and provide more accurate information

Despite these minor issues, the implementation is functional and achieves the desired outcome. I recommend addressing the suggested improvements to enhance the robustness and user experience of the feature.

});

const { mutate, isError, isSuccess } = api.profile.edit.useMutation();
const { mutate: getUploadUrl } = api.profile.getUploadUrl.useMutation();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Update remaining mutation hooks to use mutateAsync

For consistency with the updateUserPhotoUrl mutation and to improve error handling, please update the getUploadUrl and updateEmail mutation hooks to use mutateAsync:

-const { mutate: getUploadUrl } = api.profile.getUploadUrl.useMutation();
+const { mutateAsync: getUploadUrl } = api.profile.getUploadUrl.useMutation();
// ...
-const { mutate: updateEmail } = api.profile.updateEmail.useMutation();
+const { mutateAsync: updateEmail } = api.profile.updateEmail.useMutation();

Additionally, refactor the imageChange and handleNewEmailUpdate functions to use try/catch with async/await when calling these mutations. For example:

const imageChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
  // ...
  try {
    const signedUrl = await getUploadUrl({ size, type });
    const { status, fileLocation } = await uploadToUrl({
      signedUrl,
      file,
      updateUserPhotoUrl,
    });
    // ... handle success
  } catch (error) {
    // ... handle error
  }
};

const handleNewEmailUpdate = async () => {
  // ... existing checks
  try {
    await updateEmail({ newEmail });
    // ... handle success
  } catch (error) {
    // ... handle error
  }
};

This change will improve error handling and make the code more consistent throughout the component.

Also applies to: 82-82

Comment on lines +79 to 81
const { mutateAsync: updateUserPhotoUrl } =
api.profile.updateProfilePhotoUrl.useMutation();
const { mutate: updateEmail } = api.profile.updateEmail.useMutation();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codebase verification

Action Required: Update updateEmail to use mutateAsync

The updateEmail mutation hook is still using mutate. For consistent asynchronous handling, it should be updated to use mutateAsync similar to updateUserPhotoUrl.

  • File: app/(app)/settings/_client.tsx
    • Line: 81
🔗 Analysis chain

LGTM: Mutation hook updated to use mutateAsync

The updateUserPhotoUrl mutation hook has been correctly updated to use mutateAsync. This change allows for better handling of asynchronous operations.

Let's verify if similar changes have been applied to other mutation hooks in the file:


Let's attempt to verify the usage of mutateAsync in other mutation hooks by running an updated shell script:


Let's attempt to verify the usage of mutateAsync in other mutation hooks by running an updated shell script:


Let's attempt to verify the usage of mutateAsync in other mutation hooks by running an updated shell script:


Let's attempt to verify if there are other instances of mutateAsync in the mutation hooks by running an updated shell script:

🏁 Scripts executed

The 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 130 to 163
const imageChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files && e.target.files.length > 0) {
const file = e.target.files[0];
const { size, type } = file;

setProfilePhoto({ status: "loading", url: "" });

await getUploadUrl(
{ size, type },
{
onError(error) {
toast.error(
error.message ||
"Something went wrong uploading the photo, please retry.",
);
setProfilePhoto({ status: "error", url: "" });
},
async onSuccess(signedUrl) {
const { status, fileLocation } = await uploadToUrl({
signedUrl,
file,
updateUserPhotoUrl,
});

if (status === "success" && fileLocation) {
setProfilePhoto({ status: "success", url: fileLocation });
} else {
setProfilePhoto({ status: "error", url: "" });
}
},
},
);
}
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Implementation of imageChange function

The imageChange function handles file selection and initiates the upload process. However, there are some improvements that can be made:

  1. File type and size validation are missing. Add these checks before initiating the upload:
const maxSize = 10 * 1024 * 1024; // 10MB
const allowedTypes = ['image/jpeg', 'image/png', 'image/gif'];

if (file.size > maxSize) {
  toast.error("File size exceeds 10MB limit.");
  return;
}

if (!allowedTypes.includes(file.type)) {
  toast.error("Invalid file type. Please upload a JPG, GIF, or PNG image.");
  return;
}
  1. The error handling in the onError callback can be improved:
onError(error) {
  toast.error(
    error.message || "Failed to get upload URL. Please try again."
  );
  setProfilePhoto({ status: "error", url: "" });
},
  1. Add a success notification after successful upload:
if (status === "success" && fileLocation) {
  setProfilePhoto({ status: "success", url: fileLocation });
  toast.success("Profile picture updated successfully!");
} else {
  setProfilePhoto({ status: "error", url: "" });
}

These changes will improve the user experience and make the function more robust.

Comment on lines 190 to 229
<div className="mx-4 my-4">
<Field className="flex-grow">
<Label>Profile Picture</Label>
<div className="mt-3 flex items-center justify-between gap-4 px-3">
<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"
/>
<div className="pt-[30px]">
<Button
color="dark/white"
type="button"
className="h-[30px] rounded-md text-xs"
onClick={() => fileInputRef.current?.click()}
>
Change avatar
</Button>
<Input
type="file"
id="file-input"
name="user-photo"
accept="image/png, image/gif, image/jpeg"
onChange={imageChange}
className="hidden"
ref={fileInputRef}
/>
<Text className="mt-1 text-xs text-gray-500">
JPG, GIF or PNG. 10MB max.
</Text>
</div>
</div>
</Field>
</div>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

UI implementation for profile picture upload

The UI implementation for profile picture upload is good, but could be improved:

  1. Add a loading state to the Avatar component:
<Avatar
  square
  src={profilePhoto.status === "loading" ? undefined : profilePhoto.url}
  alt="Profile photo upload section"
  className={`h-16 w-16 overflow-hidden rounded-full ${
    profilePhoto.status === "loading" ? "animate-pulse" : ""
  }`}
/>
{profilePhoto.status === "loading" && (
  <div className="absolute inset-0 flex items-center justify-center">
    <span className="loading loading-spinner loading-md"></span>
  </div>
)}
  1. Disable the "Change avatar" button during upload:
<Button
  color="dark/white"
  type="button"
  className="h-[30px] rounded-md text-xs"
  onClick={() => fileInputRef.current?.click()}
  disabled={profilePhoto.status === "loading"}
>
  Change avatar
</Button>
  1. Update the file size limit in the instructions to match the actual limit:
<Text className="mt-1 text-xs text-gray-500">
  JPG, GIF or PNG. 10MB max.
</Text>

These changes will improve the user experience and provide more accurate information.

Comment on lines 112 to 120
const fileInputRef = useRef<HTMLInputElement>(null);
const [profilePhoto, setProfilePhoto] = useState<ProfilePhoto>({
status: "idle",
url: details.image,
});
const { username, name, location } = details;

const { mutate: getUploadUrl } = api.profile.getUploadUrl.useMutation();
const { mutateAsync: updateUserPhotoUrl } =
api.profile.updateProfilePhotoUrl.useMutation();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

New state and mutations added for profile picture upload

The fileInputRef, profilePhoto state, and mutations for getUploadUrl and updateUserPhotoUrl have been correctly implemented to support the profile picture upload feature.

However, there's a potential issue with the getUploadUrl mutation:

The getUploadUrl mutation is using mutate instead of mutateAsync. This could lead to issues when awaiting the mutation result. Consider changing it to:

-  const { mutate: getUploadUrl } = api.profile.getUploadUrl.useMutation();
+  const { mutateAsync: getUploadUrl } = api.profile.getUploadUrl.useMutation();

This change will allow you to properly await the mutation result in the imageChange function.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const fileInputRef = useRef<HTMLInputElement>(null);
const [profilePhoto, setProfilePhoto] = useState<ProfilePhoto>({
status: "idle",
url: details.image,
});
const { username, name, location } = details;
const { mutate: getUploadUrl } = api.profile.getUploadUrl.useMutation();
const { mutateAsync: updateUserPhotoUrl } =
api.profile.updateProfilePhotoUrl.useMutation();
const fileInputRef = useRef<HTMLInputElement>(null);
const [profilePhoto, setProfilePhoto] = useState<ProfilePhoto>({
status: "idle",
url: details.image,
});
const { username, name, location } = details;
const { mutateAsync: getUploadUrl } = api.profile.getUploadUrl.useMutation();
const { mutateAsync: updateUserPhotoUrl } =
api.profile.updateProfilePhotoUrl.useMutation();

@John-Paul-Larkin
Copy link
Member

Hi @sahilpatel2712
I think there may be some confusing here. I should have been more explicit with my previous instruction.
I see you have extracted the uploadToUrl function to a util.
What I meant was that any logic shared between the two components should be extracted to a utility function.
In other words the logic you originally copied from the settings component should be in the utility.
This will ensure the error handling will also be included for the settings page, and any change to it will be implemented for both components.
I hope this makes sense?

@sahilpatel2712
Copy link
Contributor Author

Hi @sahilpatel2712 I think there may be some confusing here. I should have been more explicit with my previous instruction. I see you have extracted the uploadToUrl function to a util. What I meant was that any logic shared between the two components should be extracted to a utility function. In other words the logic you originally copied from the settings component should be in the utility. This will ensure the error handling will also be included for the settings page, and any change to it will be implemented for both components. I hope this makes sense?

that mean I also should move imageChange function logic from both components into util function?

@John-Paul-Larkin
Copy link
Member

Hi @sahilpatel2712 I think there may be so confusing here. I should have been more explicit with my previous instruction. I see you have extracted the uploadToUrl function to a util. What I meant was that any logic shared between the two components should be extracted to a utility function. In other words the logic you originally copied from the settings component should be in the utility. This will ensure the error handling will also be included for the settings page, and any change to it will be implemented for both components. I hope this makes sense?

that mean I also should move imageChange function logic from both components into util function?

Yes, the logic which is shared should be extracted from both and combined into a utility.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Outside diff range and nitpick comments (8)
utils/fileUpload.ts (1)

27-47: LGTM: Main logic is sound with good error handling

The main function logic is well-structured and includes proper error handling for various scenarios. It's great to see that the fileLocation check suggested in the previous review has been implemented.

One minor suggestion for improvement:

Consider adding more specific error messages for different failure scenarios. For example:

if (!signedUrl) {
  toast.error("Failed to obtain upload URL. Please try again.");
  return { status: "error", fileLocation: "" };
}

// ... (after file upload)
if (!fileLocation) {
  toast.error("File uploaded successfully, but location retrieval failed.");
  return { status: "error", fileLocation: "" };
}

This will provide more informative feedback to users and potentially help with debugging.

app/(app)/settings/_client.tsx (3)

27-27: LGTM: Import statement updated for image upload functionality

The change from uploadFile to imageUploadToUrl suggests a refactoring of the file upload functionality. This is likely an improvement in the abstraction or implementation of image uploads.

Consider updating any relevant documentation to reflect this change in the image upload utility function.


107-123: LGTM: Improved image upload handling

The handleImageChange function has been significantly improved:

  • It now uses the imageUploadToUrl function for a more streamlined upload process.
  • Error handling has been enhanced with more specific error messages.
  • The use of async/await improves readability and error handling.

Consider adding more specific error handling:

 } catch (error) {
-  toast.error("Failed to upload profile photo. Please try again.");
+  toast.error(`Failed to upload profile photo: ${error.message || "Unknown error"}. Please try again.`);
   setProfilePhoto({ status: "error", url: "" });
 }

This will provide more detailed error information to the user when available.


Line range hint 126-159: Refactor handleNewEmailUpdate to use mutateAsync

The handleNewEmailUpdate function should be updated to use mutateAsync and try/catch for consistency with the improvements made to other parts of the code.

Please refactor the function as follows:

const handleNewEmailUpdate = async () => {
  if (cooldown > 0) {
    return;
  }

  if (!isValidEmail(newEmail)) {
    setEmailError("Please enter a valid email address");
    return;
  }

  setLoading(true);
  try {
    await updateEmail({ newEmail });
    setLoading(false);
    toast.success("Verification link sent to your email.");
    setSendForVerification(true);
    setCooldown(120); // Set a 2 minute cooldown
    setEmailError(""); // Clear any existing error
  } catch (error) {
    setLoading(false);
    toast.error(error.message || "Something went wrong sending the verification link.");
  }
};

This refactoring aligns with the mutateAsync pattern used elsewhere in the component and improves error handling.

app/(app)/alpha/additional-details/_client.tsx (4)

58-59: New type definitions for user details and profile photo

The UserDetails type has been updated to include an image property, and a new ProfilePhoto type has been added. These changes are appropriate for managing the new profile picture functionality.

However, consider using more specific types for the status property in ProfilePhoto:

type ProfilePhoto = {
  status: "success" | "error" | "loading" | "idle";
  url: string;
};

This will provide better type safety and autocompletion.

Also applies to: 61-63


112-120: New state and mutations for profile picture upload

The implementation of fileInputRef, profilePhoto state, and TRPC mutations looks good. However, there's a potential issue with error handling:

  1. Consider adding error handling for the TRPC mutations:
const { mutateAsync: getUploadUrl, error: getUploadUrlError } = api.profile.getUploadUrl.useMutation();
const { mutateAsync: updateUserPhotoUrl, error: updateUserPhotoUrlError } = api.profile.updateProfilePhotoUrl.useMutation();

// Later in the component:
useEffect(() => {
  if (getUploadUrlError) {
    toast.error("Failed to get upload URL. Please try again.");
  }
  if (updateUserPhotoUrlError) {
    toast.error("Failed to update profile photo. Please try again.");
  }
}, [getUploadUrlError, updateUserPhotoUrlError]);

This will provide more specific error messages to the user.


130-148: Implementation of handleImageChange function

The handleImageChange function looks good overall, but there are a few improvements we can make:

  1. Add file type and size validation before uploading:
const handleImageChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
  if (e.target.files && e.target.files.length > 0) {
    const file = e.target.files[0];
    
    // Validate file type
    const allowedTypes = ['image/jpeg', 'image/png', 'image/gif'];
    if (!allowedTypes.includes(file.type)) {
      toast.error("Invalid file type. Please upload a JPG, GIF, or PNG image.");
      return;
    }

    // Validate file size (10MB max)
    const maxSize = 10 * 1024 * 1024; // 10MB in bytes
    if (file.size > maxSize) {
      toast.error("File size exceeds 10MB limit.");
      return;
    }

    try {
      setProfilePhoto({ status: "loading", url: "" });
      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("No file selected. Please try again.");
  }
};
  1. Consider adding a success notification:
if (status === "success" && fileLocation) {
  toast.success("Profile picture updated successfully!");
}

These changes will improve the user experience and add necessary validations.


175-213: UI implementation for profile picture upload

The UI implementation looks good, but there are a few improvements we can make:

  1. Add a loading state to the Avatar component:
<Avatar
  square
  src={profilePhoto.status === "loading" ? undefined : profilePhoto.url}
  alt="Profile photo upload section"
  className={`h-16 w-16 overflow-hidden rounded-full ${
    profilePhoto.status === "loading" ? "animate-pulse" : ""
  }`}
/>
{profilePhoto.status === "loading" && (
  <div className="absolute inset-0 flex items-center justify-center">
    <span className="loading loading-spinner loading-md"></span>
  </div>
)}
  1. Disable the "Change avatar" button during upload:
<Button
  color="dark/white"
  type="button"
  className="h-[30px] rounded-md text-xs"
  onClick={() => fileInputRef.current?.click()}
  disabled={profilePhoto.status === "loading"}
>
  Change avatar
</Button>
  1. Update the file size limit in the instructions to match the actual limit:
<Text className="mt-1 text-xs text-gray-500">
  JPG, GIF or PNG. 10MB max.
</Text>

These changes will improve the user experience and provide more accurate information.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Files that changed from the base of the PR and between 4319a8d and 3f4549a.

📒 Files selected for processing (3)
  • app/(app)/alpha/additional-details/_client.tsx (6 hunks)
  • app/(app)/settings/_client.tsx (4 hunks)
  • utils/fileUpload.ts (1 hunks)
🧰 Additional context used
🔇 Additional comments (6)
utils/fileUpload.ts (2)

1-15: LGTM: Imports and type definitions are well-structured

The imports and type definitions are clear, concise, and appropriate for the file's purpose. The UploadToUrlProps and UploadResult types provide a good structure for the function parameters and return value.


1-56: Overall: Well-implemented image upload functionality with room for minor enhancements

The imageUploadToUrl function is well-structured and implements the required functionality for uploading user profile images. It includes proper type definitions, error handling, and addresses most concerns from previous reviews.

Key strengths:

  1. Clear type definitions for function props and result.
  2. Proper error handling for various scenarios.
  3. User-friendly notifications using toast messages.

Suggested improvements:

  1. Enhance initial file validation to include type and size checks.
  2. Provide more specific error messages for different failure scenarios.
  3. Add error logging for improved debugging capabilities.

These enhancements will further improve the robustness and maintainability of the code. Great job overall!

app/(app)/settings/_client.tsx (2)

Line range hint 1-424: Overall improvements with minor inconsistencies

The changes in this PR significantly enhance the Settings component, particularly in terms of image upload handling and error management. The use of mutateAsync for most mutation hooks improves asynchronous operation handling.

Key improvements:

  1. Refactored image upload functionality with better error handling.
  2. Updated mutation hooks to use mutateAsync for getUploadUrl and updateUserPhotoUrl.
  3. Increased file size limit to 10MB as per PR objectives.

Areas for further improvement:

  1. Update the updateEmail mutation hook to use mutateAsync.
  2. Refactor the handleNewEmailUpdate function to use try/catch with async/await.
  3. Ensure backend support for the increased file size limit.

Once these minor issues are addressed, the code will be more consistent and robust throughout the component.


204-204: Verify backend support for increased file size limit

The UI text has been updated to reflect an increased maximum file size of 10MB, which aligns with the PR objectives.

Please ensure that the backend (server-side code and any cloud storage services) can handle this increased file size limit. Run the following script to check for any hardcoded file size limits in the backend code:

If any results are found, make sure they are consistent with the new 10MB limit.

✅ Verification successful

Confirm all backend file size limits are updated to 10MB

The backend codebase contains a maximum file size limit of 10MB in server/api/router/profile.ts, which aligns with the UI update. To ensure consistency across the entire backend, please run the following script to verify there are no other conflicting file size limits:


Backend file size limits successfully updated to 10MB

All backend file size limits have been verified and set to 10MB in server/api/router/profile.ts. No other conflicting file size limits were found in the backend codebase.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for file size limits in backend code
rg -i '(file|upload|size|limit).*(\d+\s*(mb|kb|bytes))' --type ts --type js server/ api/

Length of output: 209


Script:

# Search for all file size limits in the backend code
rg -i '(file|upload|size|limit).*(\d+\s*(mb|kb|bytes))' --type ts --type js server/

Length of output: 159

app/(app)/alpha/additional-details/_client.tsx (2)

3-3: New imports added for profile picture functionality

The new imports seem appropriate for the added functionality:

  • useRef and useState from React for managing the file input and profile photo state.
  • Avatar and Text components for UI elements.
  • api for TRPC integration.
  • imageUploadToUrl utility function for handling file uploads.

These additions align well with the new profile picture upload feature.

Also applies to: 42-45


Line range hint 1-724: Overall assessment of profile picture upload implementation

The implementation of the profile picture upload feature in the SlideOne component is well-integrated and functional. The code structure is clear, and the use of TRPC for API calls is appropriate. However, there are a few areas for improvement:

  1. Enhanced error handling for TRPC mutations.
  2. File type and size validation before upload.
  3. Improved UI feedback during the upload process.
  4. Minor updates to the UI for better user experience.

Implementing the suggested changes will result in a more robust and user-friendly profile picture upload feature. Great job on the implementation, and these small tweaks will make it even better!

Comment on lines +17 to +25
export const imageUploadToUrl = async ({
file,
updateUserPhotoUrl,
getUploadUrl,
}: UploadToUrlProps): Promise<UploadResult> => {
if (!file) {
toast.error("Invalid file upload.");
return { status: "error", fileLocation: "" };
}
Copy link
Contributor

Choose a reason for hiding this comment

The 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:

  1. Make the error message more specific: "No file selected for upload" instead of "Invalid file upload."
  2. Add file type and size validation before proceeding with the upload.

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.

Comment on lines +48 to +56
} 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: "" };
}
};
Copy link
Contributor

Choose a reason for hiding this comment

The 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.

Comment on lines +78 to 81
const { mutateAsync: getUploadUrl } = api.profile.getUploadUrl.useMutation();
const { mutateAsync: updateUserPhotoUrl } =
api.profile.updateProfilePhotoUrl.useMutation();
const { mutate: updateEmail } = api.profile.updateEmail.useMutation();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Update remaining mutation hook to use mutateAsync

Great job updating the getUploadUrl and updateUserPhotoUrl hooks to use mutateAsync. This change improves error handling and consistency in asynchronous operations.

However, the updateEmail hook on line 81 still uses mutate. For consistency, please update it as well:

-const { mutate: updateEmail } = api.profile.updateEmail.useMutation();
+const { mutateAsync: updateEmail } = api.profile.updateEmail.useMutation();

Don't forget to refactor the handleNewEmailUpdate function to use try/catch with async/await when calling this mutation.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const { mutateAsync: getUploadUrl } = api.profile.getUploadUrl.useMutation();
const { mutateAsync: updateUserPhotoUrl } =
api.profile.updateProfilePhotoUrl.useMutation();
const { mutate: updateEmail } = api.profile.updateEmail.useMutation();
const { mutateAsync: getUploadUrl } = api.profile.getUploadUrl.useMutation();
const { mutateAsync: updateUserPhotoUrl } =
api.profile.updateProfilePhotoUrl.useMutation();
const { mutateAsync: updateEmail } = api.profile.updateEmail.useMutation();

@NiallJoeMaher
Copy link
Contributor

There is a ton of code duplication, and the design doesn't look as it does on the settings page, so I won't merge this in just yet. It will create more code than needed to maintain.

  • I think the Avatar upload code should be its own component to stop duplicated logic in 2 separate areas.
  • Anywhere there is duplicated code, there should be an attempt at reuse.

Feel free to reopen if you want to tackle it without the copy/pasting.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add avatar to the onboarding flow

3 participants