Skip to content
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

feat(backend, frontend): upload avatar #165

Merged
merged 18 commits into from
Mar 12, 2025
Merged

feat(backend, frontend): upload avatar #165

merged 18 commits into from
Mar 12, 2025

Conversation

ZHallen122
Copy link
Collaborator

@ZHallen122 ZHallen122 commented Mar 6, 2025

Summary by CodeRabbit

  • New Features

    • Enabled avatar management so users can now upload and update their profile images.
    • Introduced a secure media endpoint for retrieving media files with enhanced validation.
    • Added new operations to improve user profile management through interactive settings.
  • Refactor

    • Streamlined the user settings interface with modern, consistent components.
    • Replaced outdated elements and removed legacy message editing features for a cleaner experience.
    • Enhanced authentication flows with a new mechanism to refresh user information.

Videos:
https://jam.dev/c/c8fc225a-d3e1-4ced-9deb-411fe67918ed

@ZHallen122 ZHallen122 requested a review from Sma1lboy March 6, 2025 01:45
Copy link
Contributor

coderabbitai bot commented Mar 6, 2025

Walkthrough

This PR integrates avatar upload functionality and media management across the system. In the backend, the UploadModule is added to chat and user modules, along with new GraphQL types, resolvers, and service methods for avatar uploads. Utility functions for media directories are introduced. On the frontend, a new AvatarUploader component, updates to GraphQL queries/mutations, and adjustments to settings and sidebar components enable users to manage their avatars. Additionally, a new media serving API route is added and minor cleanup and refactoring are performed in several components.

Changes

File(s) Change Summary
backend/src/chat/chat.module.ts, backend/src/user/user.module.ts Added UploadModule to module imports to enable file upload functionality.
backend/src/user/dto/upload-avatar.input.ts, backend/src/user/user.model.ts, backend/src/user/user.resolver.ts, backend/src/user/user.service.ts Introduced avatar upload features: new UploadAvatarInput, an optional avatarUrl in the user model, AvatarUploadResponse type with associated mutations and queries, and an updateAvatar method in UserService leveraging an injected UploadService.
codefox-common/src/common-path.ts Added helper functions: getMediaDir, getMediaPath, and getMediaAvatarsDir for structured media directory management.
frontend/src/app/api/media/[...path]/route.ts Implemented a new GET API route for media file delivery with path normalization, file size checking, and content type handling.
frontend/src/app/api/runProject/route.ts Added new imports and defined a constant (isWindows) for platform detection, setting up for potential project photo updates.
frontend/src/graphql/request.ts, frontend/src/graphql/type.tsx, frontend/src/graphql/schema.gql Extended GraphQL operations and schema to support avatar uploads and retrieval, with new queries (GET_USER_AVATAR) and mutations (UPLOAD_AVATAR) and added avatarUrl fields.
frontend/src/components/avatar-uploader.tsx, frontend/src/components/chat/index.tsx, frontend/src/app/(main)/settings/page.tsx, frontend/src/components/detail-settings.tsx, frontend/src/components/settings/settings.tsx, frontend/src/components/file-sidebar.tsx, frontend/src/components/sidebar.tsx, frontend/src/components/user-settings-bar.tsx Updated UI components: introduced the AvatarUploader, replaced EditUsernameForm with UserSetting, renamed UserSettings to UserSettingsBar, and refined settings and sidebar functionality.
frontend/src/providers/AuthProvider.tsx Added a refreshUserInfo method to the authentication context to enable explicit user info refresh.
frontend/src/components/chat/chat-panel.tsx Removed commented-out code related to message editing to clean up the component.

Sequence Diagram(s)

sequenceDiagram
    participant U as User (AvatarUploader)
    participant G as GraphQL API
    participant R as UserResolver
    participant S as UserService
    participant US as UploadService
    participant DB as Database

    U->>G: Trigger uploadAvatar mutation with file
    G->>R: Forward request to resolver
    R->>S: Call updateAvatar(userId, file)
    S->>US: Validate and buffer file, then upload file
    US-->>S: Return uploaded file URL
    S->>DB: Update user record with avatarUrl
    DB-->>S: Confirm update
    S-->>R: Return AvatarUploadResponse { success, avatarUrl }
    R-->>G: Return response
    G-->>U: Deliver upload result for UI update
Loading

Possibly related PRs

Suggested reviewers

  • Sma1lboy
  • NarwhalChen

Poem

I'm a bunny hopping through lines of code,
Uploading avatars down a bright new road.
Files and features now dance in delight,
Settings and media shining ever so bright.
With carrots and code, I cheer with glee,
For a snappy system that's set free!
🥕🐇 Happy coding, one and all!

Warning

There were issues while running some tools. Please review the errors and either fix the tool’s configuration or disable the tool if it’s a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

frontend/src/components/user-settings-bar.tsx

Oops! Something went wrong! :(

ESLint: 8.57.1

ESLint couldn't find the config "next/core-web-vitals" to extend from. Please check that the name of the config is correct.

The config "next/core-web-vitals" was referenced from the config file in "/frontend/.eslintrc.json".

If you still have problems, please stop by https://eslint.org/chat/help to chat with the team.

frontend/src/components/avatar-uploader.tsx

Oops! Something went wrong! :(

ESLint: 8.57.1

ESLint couldn't find the config "next/core-web-vitals" to extend from. Please check that the name of the config is correct.

The config "next/core-web-vitals" was referenced from the config file in "/frontend/.eslintrc.json".

If you still have problems, please stop by https://eslint.org/chat/help to chat with the team.

✨ Finishing Touches
  • 📝 Generate Docstrings

🪧 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 generate docstrings to generate docstrings for this 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.

@Sma1lboy Sma1lboy changed the title Feat upload avatar feat(backend): upload avatar Mar 7, 2025
@ZHallen122 ZHallen122 changed the title feat(backend): upload avatar feat(backend, frontend): upload avatar Mar 7, 2025
@ZHallen122 ZHallen122 marked this pull request as ready for review March 7, 2025 04:49
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: 8

🔭 Outside diff range comments (1)
frontend/src/components/edit-username-form.tsx (1)

82-86: ⚠️ Potential issue

Inconsistency between local storage and auth context

The onSubmit function updates the username in localStorage and dispatches a storage event, but the component now uses the authentication context for user data. This might lead to inconsistencies between the local storage and the actual user state.

Consider updating the user through an API call and refreshing the user context:

function onSubmit(values: z.infer<typeof formSchema>) {
-  localStorage.setItem('ollama_user', values.username);
-  window.dispatchEvent(new Event('storage'));
+  // Call API to update username
+  updateUsernameMutation({ variables: { username: values.username } })
+    .then(() => {
+      refreshUserInfo(); // Refresh user information in context
+      toast.success('Name updated successfully');
+    })
+    .catch((error) => {
+      toast.error('Failed to update name: ' + error.message);
+    });
  toast.success('Name updated successfully');
}
🧹 Nitpick comments (10)
frontend/src/graphql/type.tsx (1)

367-367: Unusual property placement

The avatarUrl property is positioned before the __typename field, which is unconventional as __typename typically comes first in GraphQL object types. While this doesn't affect functionality, consider maintaining a consistent ordering of fields across all types.

-  avatarUrl: string;
   __typename: 'User';
+  avatarUrl: string;
backend/src/user/user.service.ts (1)

45-62: Consider cleaning up old avatar files

The updateAvatar method is well-implemented with appropriate error handling and validation. However, it doesn't handle the case where a user already has an avatar. Consider cleaning up the old avatar file to avoid accumulating unused files in storage.

async updateAvatar(userId: string, file: Promise<FileUpload>): Promise<User> {
  // Get the user
  const user = await this.userRepository.findOneBy({ id: userId });
  if (!user) {
    throw new Error('User not found');
  }

+ // Check if user already has an avatar and delete old one if needed
+ if (user.avatarUrl) {
+   try {
+     await this.uploadService.deleteFile(user.avatarUrl);
+   } catch (error) {
+     console.warn(`Failed to delete old avatar: ${error.message}`);
+     // Continue with upload even if deletion fails
+   }
+ }

  // Validate and convert file to buffer
  const uploadedFile = await file;
  const { buffer, mimetype } = await validateAndBufferFile(uploadedFile);

  // Upload the validated buffer to storage
  const result = await this.uploadService.upload(buffer, mimetype, 'avatars');

  // Update the user's avatar URL
  user.avatarUrl = result.url;
  return this.userRepository.save(user);
}
frontend/src/app/api/media/[...path]/route.ts (3)

42-44: Use async file operations instead of blocking synchronous calls.

The current implementation uses fs.readFileSync which blocks the Node.js event loop and can impact performance, especially for larger files.

Use non-blocking async file operations instead:

- // Read the file
- const fileBuffer = fs.readFileSync(filePath);
+ // Read the file asynchronously
+ const fileBuffer = await fs.promises.readFile(filePath);

19-40: Move debugging logic to a separate function and consider environment-based logging.

The current debug logging code is mixed with the main logic and always executes in production, which isn't ideal.

Extract debug logic to a separate function and consider adding environment checks:

// Check if the file exists
if (!fs.existsSync(filePath)) {
-  // Log directory contents for debugging
-  try {
-    if (fs.existsSync(mediaDir)) {
-      const avatarsDir = path.join(mediaDir, 'avatars');
-      if (fs.existsSync(avatarsDir)) {
-        console.log(
-          'Avatars directory contents:',
-          fs.readdirSync(avatarsDir)
-        );
-      } else {
-        console.log('Avatars directory does not exist');
-      }
-    } else {
-      console.log('Media directory does not exist');
-    }
-  } catch (err) {
-    console.error('Error reading directory:', err);
-  }
+  // Log directory contents only in development
+  if (process.env.NODE_ENV === 'development') {
+    logDirectoryContents(mediaDir);
+  }

  return new Response('File not found', { status: 404 });
}

+ // Separate function for directory logging
+ function logDirectoryContents(mediaDir) {
+   try {
+     if (fs.existsSync(mediaDir)) {
+       const avatarsDir = path.join(mediaDir, 'avatars');
+       if (fs.existsSync(avatarsDir)) {
+         console.log(
+           'Avatars directory contents:',
+           fs.readdirSync(avatarsDir)
+         );
+       } else {
+         console.log('Avatars directory does not exist');
+       }
+     } else {
+       console.log('Media directory does not exist');
+     }
+   } catch (err) {
+     console.error('Error reading directory:', err);
+   }
+ }

57-63: Add content disposition header for security.

Adding a Content-Disposition header can help prevent certain types of attacks (like MIME-type confusion) and provide a better download experience when needed.

Add Content-Disposition header for inline viewing:

// Return the file with appropriate headers
return new Response(fileBuffer, {
  headers: {
    'Content-Type': contentType,
    'Cache-Control': 'public, max-age=31536000',
+   'Content-Disposition': 'inline',
  },
});
frontend/src/components/user-settings.tsx (2)

80-89: Consider removing the setTimeout from handleSettingsClick.

Using setTimeout with a 0ms delay to dispatch an event after navigation is a bit of a workaround pattern. Consider more direct approaches.

You could try to dispatch the event directly, or if there's a specific reason for the delay (like waiting for the route change to complete), add a comment explaining why.

const handleSettingsClick = () => {
  // First navigate using Next.js router
  router.push('/chat?id=setting');

  // Then dispatch the event
- setTimeout(() => {
-   const event = new Event(EventEnum.SETTING);
-   window.dispatchEvent(event);
- }, 0);
+ // Dispatch event after navigation
+ const event = new Event(EventEnum.SETTING);
+ window.dispatchEvent(event);
+ 
+ // If you need to keep the setTimeout for specific reasons, add a comment explaining why:
+ // setTimeout(() => {
+ //   const event = new Event(EventEnum.SETTING);
+ //   window.dispatchEvent(event);
+ // }, 0); // Using setTimeout to ensure event fires after navigation completes
};

101-106: Add error handling for avatar image loading.

Missing error handling for the AvatarImage component could result in a broken UI if the image fails to load.

Add an onError handler to handle image loading failures:

{/* Use normalized avatar URL */}
<AvatarImage
  src={normalizedAvatarUrl}
  alt="User"
  key={user?.avatarUrl}
+ onError={(e) => {
+   // Reset src to empty to show fallback
+   e.currentTarget.src = '';
+ }}
/>
frontend/src/components/avatar-uploader.tsx (3)

57-60: Extract file size limit to a constant.

The 5MB file size limit is hardcoded. It would be better to extract it to a constant for maintainability.

Extract the file size limit to a constant:

+ // Constants
+ const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB
+ const ALLOWED_FILE_TYPES = ['image/jpeg', 'image/png', 'image/webp'];

// ...

// Check file size on client-side as well (5MB limit)
- if (file.size > 5 * 1024 * 1024) {
+ if (file.size > MAX_FILE_SIZE) {
  toast.error('File size exceeds the maximum allowed limit (5MB)');
  return;
}

// ...

// Check file type on client-side
const fileType = file.type;
- if (!['image/jpeg', 'image/png', 'image/webp'].includes(fileType)) {
+ if (!ALLOWED_FILE_TYPES.includes(fileType)) {
  toast.error('Only JPEG, PNG, and WebP files are allowed');
  return;
}

48-50: Add cleanup for FileReader preview URL.

The component creates a preview URL but doesn't clean it up when the component unmounts, which could potentially lead to memory leaks.

Add a useEffect to clean up the preview URL:

const [uploadAvatar, { loading }] = useMutation(UPLOAD_AVATAR);
const fileInputRef = useRef<HTMLInputElement>(null);
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
const { refreshUserInfo } = useAuthContext();

+ // Clean up preview URL when component unmounts or when previewUrl changes
+ useEffect(() => {
+   return () => {
+     // Clean up any object URLs to avoid memory leaks
+     if (previewUrl?.startsWith('blob:')) {
+       URL.revokeObjectURL(previewUrl);
+     }
+   };
+ }, [previewUrl]);

Don't forget to add the import:

- import React, { useRef, useState } from 'react';
+ import React, { useRef, useState, useEffect } from 'react';

76-95: Add better loading indicator during upload.

The component shows "Uploading..." on the button, but there's no visual indication that the avatar is being uploaded.

Add a loading overlay on the avatar during upload:

return (
  <div className="flex flex-col items-center gap-4">
    <input
      type="file"
      ref={fileInputRef}
      onChange={handleFileChange}
      accept="image/*"
      className="hidden"
    />
-   <Avatar className="w-24 h-24 cursor-pointer" onClick={triggerFileInput}>
+   <div className="relative">
+     <Avatar className="w-24 h-24 cursor-pointer" onClick={triggerFileInput}>
        <AvatarImage src={displayUrl} alt="User Avatar" />
        <AvatarFallback>{avatarFallback}</AvatarFallback>
-     </Avatar>
+     </Avatar>
+     {loading && (
+       <div className="absolute inset-0 bg-black bg-opacity-50 rounded-full flex items-center justify-center">
+         <div className="animate-spin h-6 w-6 border-2 border-white border-t-transparent rounded-full"></div>
+       </div>
+     )}
+   </div>
    <Button
      onClick={triggerFileInput}
      disabled={loading}
      size="sm"
      variant="outline"
    >
      {loading ? 'Uploading...' : 'Change Avatar'}
    </Button>
  </div>
);
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4e0987d and cc0e6b4.

📒 Files selected for processing (15)
  • backend/src/chat/chat.module.ts (1 hunks)
  • backend/src/user/dto/upload-avatar.input.ts (1 hunks)
  • backend/src/user/user.model.ts (1 hunks)
  • backend/src/user/user.module.ts (1 hunks)
  • backend/src/user/user.resolver.ts (3 hunks)
  • backend/src/user/user.service.ts (2 hunks)
  • codefox-common/src/common-path.ts (1 hunks)
  • frontend/src/app/api/media/[...path]/route.ts (1 hunks)
  • frontend/src/app/api/runProject/route.ts (2 hunks)
  • frontend/src/components/avatar-uploader.tsx (1 hunks)
  • frontend/src/components/edit-username-form.tsx (3 hunks)
  • frontend/src/components/user-settings.tsx (4 hunks)
  • frontend/src/graphql/request.ts (2 hunks)
  • frontend/src/graphql/type.tsx (1 hunks)
  • frontend/src/providers/AuthProvider.tsx (4 hunks)
🔇 Additional comments (19)
backend/src/user/user.module.ts (1)

10-10: Import and configuration for avatar upload feature

Good addition of the UploadModule to handle file uploads for user avatars. This is consistent with the new avatarUrl field in the User model.

Also applies to: 18-18

backend/src/chat/chat.module.ts (1)

13-13:

❓ Verification inconclusive

Consistent module integration

Adding the UploadModule to the ChatModule is consistent with the changes in the UserModule. This suggests a unified approach to file uploads across different parts of the application.

Also applies to: 20-20


🏁 Script executed:

#!/bin/bash
# Checking if chat-related upload functionality is implemented

# Look for upload-related methods in chat services and resolvers
echo "Checking chat resolvers and services for upload methods..."
rg -A 10 -B 10 "upload|Upload" backend/src/chat/*resolver.ts backend/src/chat/*service.ts

# Look for file/media fields in message-related models
echo "Checking message models for file/media fields..."
rg "@Field|@Column" -A 3 "file|image|media|attachment" backend/src/chat/message.model.ts

Length of output: 1238


Action: Please Verify Chat Upload Integration Consistency

The inclusion of the UploadModule in the ChatModule (lines 13 and 20) appears to follow the approach implemented in the UserModule for handling file uploads. However, our automated script did not reveal explicit upload-related methods in the chat resolvers or services, which might indicate that the functionality is handled differently (or deferred to shared components). Given the inconclusive automated output, please perform a manual review to ensure that:

  • The chat module’s upload functionality is correctly integrated and activated (either directly or via shared services).
  • There’s consistency between how file uploads are managed in ChatModule versus UserModule.
  • No critical upload handlers or method references in chat-specific components are inadvertently missing.
backend/src/user/user.model.ts (1)

31-33: Properly implemented optional field

The avatarUrl field is correctly implemented as nullable in the database and optional in the TypeScript type. This is a good practice when adding new fields to existing entities as it won't break existing records.

backend/src/user/dto/upload-avatar.input.ts (1)

1-8: LGTM! Well-structured input type for avatar uploads.

The UploadAvatarInput class is correctly defined with the appropriate GraphQL decorators and uses the FileUpload type from graphql-upload-minimal as expected. This follows NestJS best practices for handling file uploads in GraphQL.

frontend/src/app/api/runProject/route.ts (2)

11-13: Good addition for cross-platform compatibility.

Adding OS detection to enable Windows compatibility is a good improvement.


87-89: Excellent platform-specific command handling.

The conditional commands handle Windows vs Unix/Linux file system operations appropriately, maintaining identical functionality across platforms.

frontend/src/graphql/request.ts (2)

104-104: Good addition of avatarUrl field to user info.

Adding the avatarUrl field to the existing query is the correct approach to extend user information.


255-270: Well-implemented GraphQL operations for avatar functionality.

The new mutation and query for avatar management follow the established patterns in the codebase and provide all necessary fields.

Some considerations:

  1. The UPLOAD_AVATAR mutation correctly uses the GraphQL Upload scalar
  2. The success field in the response allows for proper error handling
  3. The GET_USER_AVATAR query accepts a userId parameter to fetch specific user avatars
backend/src/user/user.resolver.ts (3)

32-39: Good implementation of AvatarUploadResponse type

This class is well-structured with appropriate fields for success status and avatarUrl.


91-113: Well-implemented avatar upload mutation with proper error handling

The implementation includes:

  • Clear documentation
  • Proper authentication via user token
  • GraphQLUpload type for file handling
  • Comprehensive error handling with detailed logging

This approach ensures errors are properly caught, logged, and propagated to the GraphQL error handler.


118-122: Consider access control for getUserAvatar query

While the implementation is correct, consider whether any user should be able to access any other user's avatar URL. If this is intended for public profiles, then the current implementation is fine. Otherwise, you might want to add authorization checks.

backend/src/user/user.service.ts (1)

14-14: Good dependency injection of UploadService

The UploadService is properly injected as a dependency, following NestJS best practices.

frontend/src/components/edit-username-form.tsx (3)

60-61: Good use of authentication context

Using the authentication context is a better approach than directly accessing localStorage for user data.


68-73: Well implemented user data initialization

The useEffect hook is properly set up to initialize username and avatar URL from authenticated user data.


111-115: Well implemented AvatarUploader integration

The AvatarUploader component is properly integrated with all necessary props: current avatar URL, fallback avatar, and a callback for handling avatar changes.

frontend/src/providers/AuthProvider.tsx (3)

26-26: Good interface extension

Adding the refreshUserInfo method to the AuthContextValue interface properly exposes this functionality to consumers of the context.


87-89: Well implemented refreshUserInfo method

The refreshUserInfo method is:

  • Properly memoized with useCallback
  • Simple and focused (calls the existing fetchUserInfo method)
  • Returns a boolean to indicate success/failure

This approach supports updating the user information after avatar changes and other user data updates.


196-196: Context value properly updated

The refreshUserInfo method is correctly added to the context provider value, making it accessible to components that consume the AuthContext.

frontend/src/components/avatar-uploader.tsx (1)

11-34: Extract normalizeAvatarUrl to a shared utility.

This function is duplicated in both user-settings.tsx and avatar-uploader.tsx. Following the DRY principle, it should be extracted to a shared utility file.

@@ -364,6 +364,7 @@ export type UpdateProjectPhotoInput = {
};

export type User = {
avatarUrl: string;
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Type inconsistency with backend model

The avatarUrl property is defined as a required string in the frontend type definition, but it's defined as optional (avatarUrl?: string) in the backend model. This could lead to runtime type errors if the backend returns null for users without an avatar.


🏁 Script executed:

#!/bin/bash
# Verify if the avatarUrl property is handled correctly in the front-end code

# Check if there are any null checks for avatarUrl in the codebase
echo "Checking for null checks on avatarUrl..."
rg "avatarUrl(?:\s+)?\?\?" frontend/src/

Length of output: 133


Action Required: Resolve Type Inconsistency for avatarUrl

The current type definition in frontend/src/graphql/type.tsx (line 367) declares avatarUrl as a required string, while the backend model defines it as optional. Our verification via a regex search for null-coalescing checks (using rg "avatarUrl(?:\s+)?\?\?") returned no results, indicating that there are no safeguards in place to handle a null value. This discrepancy can lead to runtime errors if the backend returns null for users without an avatar.

Recommendations:

  • Update the Type Definition: Change the line from
    avatarUrl: string;
    to
    avatarUrl?: string;
    to directly match the backend model.
  • Alternatively, Add Null Checks: If the frontend logic must treat avatarUrl as required, implement comprehensive null checks to provide fallback values wherever avatarUrl is used.

Comment on lines 31 to 33
@Field()
@Column({ nullable: true })
avatarUrl?: string;
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Type inconsistency with frontend model

The backend defines avatarUrl as optional (avatarUrl?: string), but the frontend GraphQL type in frontend/src/graphql/type.tsx defines it as required (avatarUrl: string). This mismatch could lead to runtime errors.


🏁 Script executed:

#!/bin/bash
# Checking for potential handling of null avatarUrls in frontend code

# Search for GraphQL queries that include avatarUrl
echo "Checking GraphQL queries that include avatarUrl..."
rg "avatarUrl" frontend/src/graphql/request.ts

# Check component usage of avatarUrl for null handling
echo "Checking components that might use avatarUrl..."
rg -A 5 -B 5 "avatarUrl" frontend/src/components/

Length of output: 10806


Address GraphQL Type Mismatch for avatarUrl

  • The backend model (backend/src/user/user.model.ts) marks avatarUrl as optional (nullable).
  • The frontend, however, defines the corresponding GraphQL type in frontend/src/graphql/type.tsx as required.
  • While the UI components (e.g., in avatar-uploader.tsx, edit-username-form.tsx, and user-settings.tsx) normalize and provide default values when avatarUrl is not set, the type declaration still implies the value should always be present.
  • Recommendation: Update the frontend GraphQL type to allow avatarUrl to be null/undefined (for example, using avatarUrl?: string or avatarUrl: string | null) or modify the backend to guarantee a non-null string. This alignment will prevent potential runtime errors and ensure consistency across the data flow.

Comment on lines +48 to +53
export const getMediaDir = (): string =>
ensureDir(path.join(getRootDir(), 'media'));
export const getMediaPath = (modelName: string): string =>
path.join(getModelsDir(), modelName);
export const getMediaAvatarsDir = (): string =>
ensureDir(path.join(getMediaDir(), 'avatars'));
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Consistent implementation of media directory utilities.

The new media directory utility functions follow the established pattern in the codebase and use the existing ensureDir helper for creating directories when needed.

One minor issue:


🏁 Script executed:

#!/bin/bash
# Check if getMediaPath is using the correct base directory

# The function references getModelsDir instead of getMediaDir
echo "Possible inconsistency in getMediaPath implementation:"
grep -n "getMediaPath" codefox-common/src/common-path.ts

Length of output: 237


Media Directory Path Inconsistency – Action Needed

The new media directory utility functions are consistent overall, but there’s a minor discrepancy: the getMediaPath function uses getModelsDir instead of the expected getMediaDir (as used in getMediaAvatarsDir). Please confirm whether this is intentional; if not, update the function to use getMediaDir for consistency with the media utilities pattern.

Suggested diff:

- export const getMediaPath = (modelName: string): string =>
-   path.join(getModelsDir(), modelName);
+ export const getMediaPath = (modelName: string): string =>
+   path.join(getMediaDir(), modelName);
📝 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
export const getMediaDir = (): string =>
ensureDir(path.join(getRootDir(), 'media'));
export const getMediaPath = (modelName: string): string =>
path.join(getModelsDir(), modelName);
export const getMediaAvatarsDir = (): string =>
ensureDir(path.join(getMediaDir(), 'avatars'));
export const getMediaDir = (): string =>
ensureDir(path.join(getRootDir(), 'media'));
export const getMediaPath = (modelName: string): string =>
path.join(getMediaDir(), modelName);
export const getMediaAvatarsDir = (): string =>
ensureDir(path.join(getMediaDir(), 'avatars'));

Comment on lines 1 to 70
// For App Router: app/api/media/[...path]/route.ts
import { NextRequest } from 'next/server';
import fs from 'fs';
import path from 'path';
import { getMediaDir } from 'codefox-common';

export async function GET(
request: NextRequest,
{ params }: { params: { path: string[] } }
) {
try {
// Get the media directory path
const mediaDir = getMediaDir();

// Construct the full path to the requested file
const filePath = path.join(mediaDir, ...params.path);

// Check if the file exists
if (!fs.existsSync(filePath)) {
// Log directory contents for debugging
try {
if (fs.existsSync(mediaDir)) {
const avatarsDir = path.join(mediaDir, 'avatars');
if (fs.existsSync(avatarsDir)) {
console.log(
'Avatars directory contents:',
fs.readdirSync(avatarsDir)
);
} else {
console.log('Avatars directory does not exist');
}
} else {
console.log('Media directory does not exist');
}
} catch (err) {
console.error('Error reading directory:', err);
}

return new Response('File not found', { status: 404 });
}

// Read the file
const fileBuffer = fs.readFileSync(filePath);

// Determine content type based on file extension
const ext = path.extname(filePath).toLowerCase();
const contentTypeMap: Record<string, string> = {
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.png': 'image/png',
'.webp': 'image/webp',
'.gif': 'image/gif',
};

const contentType = contentTypeMap[ext] || 'application/octet-stream';

// Return the file with appropriate headers
return new Response(fileBuffer, {
headers: {
'Content-Type': contentType,
'Cache-Control': 'public, max-age=31536000',
},
});
} catch (error) {
console.error('Error serving media file:', error);
return new Response(`Error serving file: ${error.message}`, {
status: 500,
});
}
}
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 path sanitization to prevent directory traversal attacks.

The current implementation doesn't validate or sanitize the path parameters, which could potentially allow directory traversal attacks where malicious users attempt to access files outside the intended media directory.

Add path sanitization by verifying that the final resolved path is still within the media directory:

// Construct the full path to the requested file
const filePath = path.join(mediaDir, ...params.path);

+ // Check for directory traversal attempts
+ const normalizedPath = path.normalize(filePath);
+ if (!normalizedPath.startsWith(mediaDir)) {
+   console.error('Possible directory traversal attempt:', filePath);
+   return new Response('Access denied', { status: 403 });
+ }
📝 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
// For App Router: app/api/media/[...path]/route.ts
import { NextRequest } from 'next/server';
import fs from 'fs';
import path from 'path';
import { getMediaDir } from 'codefox-common';
export async function GET(
request: NextRequest,
{ params }: { params: { path: string[] } }
) {
try {
// Get the media directory path
const mediaDir = getMediaDir();
// Construct the full path to the requested file
const filePath = path.join(mediaDir, ...params.path);
// Check if the file exists
if (!fs.existsSync(filePath)) {
// Log directory contents for debugging
try {
if (fs.existsSync(mediaDir)) {
const avatarsDir = path.join(mediaDir, 'avatars');
if (fs.existsSync(avatarsDir)) {
console.log(
'Avatars directory contents:',
fs.readdirSync(avatarsDir)
);
} else {
console.log('Avatars directory does not exist');
}
} else {
console.log('Media directory does not exist');
}
} catch (err) {
console.error('Error reading directory:', err);
}
return new Response('File not found', { status: 404 });
}
// Read the file
const fileBuffer = fs.readFileSync(filePath);
// Determine content type based on file extension
const ext = path.extname(filePath).toLowerCase();
const contentTypeMap: Record<string, string> = {
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.png': 'image/png',
'.webp': 'image/webp',
'.gif': 'image/gif',
};
const contentType = contentTypeMap[ext] || 'application/octet-stream';
// Return the file with appropriate headers
return new Response(fileBuffer, {
headers: {
'Content-Type': contentType,
'Cache-Control': 'public, max-age=31536000',
},
});
} catch (error) {
console.error('Error serving media file:', error);
return new Response(`Error serving file: ${error.message}`, {
status: 500,
});
}
}
// For App Router: app/api/media/[...path]/route.ts
import { NextRequest } from 'next/server';
import fs from 'fs';
import path from 'path';
import { getMediaDir } from 'codefox-common';
export async function GET(
request: NextRequest,
{ params }: { params: { path: string[] } }
) {
try {
// Get the media directory path
const mediaDir = getMediaDir();
// Construct the full path to the requested file
const filePath = path.join(mediaDir, ...params.path);
// Check for directory traversal attempts
const normalizedPath = path.normalize(filePath);
if (!normalizedPath.startsWith(mediaDir)) {
console.error('Possible directory traversal attempt:', filePath);
return new Response('Access denied', { status: 403 });
}
// Check if the file exists
if (!fs.existsSync(filePath)) {
// Log directory contents for debugging
try {
if (fs.existsSync(mediaDir)) {
const avatarsDir = path.join(mediaDir, 'avatars');
if (fs.existsSync(avatarsDir)) {
console.log(
'Avatars directory contents:',
fs.readdirSync(avatarsDir)
);
} else {
console.log('Avatars directory does not exist');
}
} else {
console.log('Media directory does not exist');
}
} catch (err) {
console.error('Error reading directory:', err);
}
return new Response('File not found', { status: 404 });
}
// Read the file
const fileBuffer = fs.readFileSync(filePath);
// Determine content type based on file extension
const ext = path.extname(filePath).toLowerCase();
const contentTypeMap: Record<string, string> = {
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.png': 'image/png',
'.webp': 'image/webp',
'.gif': 'image/gif',
};
const contentType = contentTypeMap[ext] || 'application/octet-stream';
// Return the file with appropriate headers
return new Response(fileBuffer, {
headers: {
'Content-Type': contentType,
'Cache-Control': 'public, max-age=31536000',
},
});
} catch (error) {
console.error('Error serving media file:', error);
return new Response(`Error serving file: ${error.message}`, {
status: 500,
});
}
}

Comment on lines 23 to 46
// Avatar URL normalization helper
function normalizeAvatarUrl(avatarUrl: string | null | undefined): string {
if (!avatarUrl) return '';

// Check if it's already an absolute URL (S3 case)
if (avatarUrl.startsWith('http://') || avatarUrl.startsWith('https://')) {
return avatarUrl;
}

// Check if it's a relative media path
if (avatarUrl.startsWith('media/')) {
// Convert to API route path
return `/api/${avatarUrl}`;
}

// Handle paths that might not have the media/ prefix
if (avatarUrl.includes('avatars/')) {
const parts = avatarUrl.split('avatars/');
return `/api/media/avatars/${parts[parts.length - 1]}`;
}

// Return as is for other cases
return avatarUrl;
}
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

Extract normalizeAvatarUrl to a shared utility.

This function is duplicated in both user-settings.tsx and avatar-uploader.tsx. Following the DRY principle, it should be extracted to a shared utility file.

Create a new utility file (e.g., frontend/src/utils/avatar-helpers.ts) and move the function there:

// In frontend/src/utils/avatar-helpers.ts
export function normalizeAvatarUrl(avatarUrl: string | null | undefined): string {
  if (!avatarUrl) return '';

  // Check if it's already an absolute URL (S3 case)
  if (avatarUrl.startsWith('http://') || avatarUrl.startsWith('https://')) {
    return avatarUrl;
  }

  // Check if it's a relative media path
  if (avatarUrl.startsWith('media/')) {
    // Convert to API route path
    return `/api/${avatarUrl}`;
  }

  // Handle paths that might not have the media/ prefix
  if (avatarUrl.includes('avatars/')) {
    const parts = avatarUrl.split('avatars/');
    return `/api/media/avatars/${parts[parts.length - 1]}`;
  }

  // Return as is for other cases
  return avatarUrl;
}

Then import it in both files:

- // Avatar URL normalization helper
- function normalizeAvatarUrl(avatarUrl: string | null | undefined): string {
-   if (!avatarUrl) return '';
-
-   // Check if it's already an absolute URL (S3 case)
-   if (avatarUrl.startsWith('http://') || avatarUrl.startsWith('https://')) {
-     return avatarUrl;
-   }
-
-   // Check if it's a relative media path
-   if (avatarUrl.startsWith('media/')) {
-     // Convert to API route path
-     return `/api/${avatarUrl}`;
-   }
-
-   // Handle paths that might not have the media/ prefix
-   if (avatarUrl.includes('avatars/')) {
-     const parts = avatarUrl.split('avatars/');
-     return `/api/media/avatars/${parts[parts.length - 1]}`;
-   }
-
-   // Return as is for other cases
-   return avatarUrl;
- }
+ import { normalizeAvatarUrl } from '../utils/avatar-helpers';

Comment on lines +69 to +74
// Create a preview URL
const reader = new FileReader();
reader.onload = () => {
setPreviewUrl(reader.result as string);
};
reader.readAsDataURL(file);
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 error handling for FileReader operations.

The FileReader operations lack error handling, which could lead to unhandled exceptions if file reading fails.

Add error handling for the FileReader:

// Create a preview URL
const reader = new FileReader();
reader.onload = () => {
  setPreviewUrl(reader.result as string);
};
+ reader.onerror = () => {
+   toast.error('Failed to read file');
+   console.error('FileReader error:', reader.error);
+ };
reader.readAsDataURL(file);
📝 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
// Create a preview URL
const reader = new FileReader();
reader.onload = () => {
setPreviewUrl(reader.result as string);
};
reader.readAsDataURL(file);
// Create a preview URL
const reader = new FileReader();
reader.onload = () => {
setPreviewUrl(reader.result as string);
};
reader.onerror = () => {
toast.error('Failed to read file');
console.error('FileReader error:', reader.error);
};
reader.readAsDataURL(file);

Comment on lines +101 to +103
if (error.graphQLErrors && error.graphQLErrors.length > 0) {
errorMessage = error.graphQLErrors[0].message;
}
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 null check before accessing error.graphQLErrors.

Accessing properties without null checks could lead to runtime errors.

Add null check before accessing error.graphQLErrors:

let errorMessage = 'Failed to upload avatar';
- if (error.graphQLErrors && error.graphQLErrors.length > 0) {
+ if (error?.graphQLErrors?.length > 0) {
  errorMessage = error.graphQLErrors[0].message;
}
📝 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 (error.graphQLErrors && error.graphQLErrors.length > 0) {
errorMessage = error.graphQLErrors[0].message;
}
let errorMessage = 'Failed to upload avatar';
if (error?.graphQLErrors?.length > 0) {
errorMessage = error.graphQLErrors[0].message;
}

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

🧹 Nitpick comments (5)
frontend/src/graphql/type.tsx (1)

588-596: Ensure consistency in AvatarUploadResponseResolvers.
If avatarUrl can realistically be null, consider matching the resolver signature with a Maybe<String> return. Otherwise, returning a required string is fine if an actual URL is always guaranteed.

frontend/src/app/api/media/[...path]/route.ts (4)

10-19: Good directory traversal check with path normalization.
This logic is vital for preventing unauthorized file access. One note: on certain Windows deployments, case or drive letters might produce edge cases. If you plan on cross-platform usage, keep an eye on these.


20-32: File extension allowlist is an excellent security measure.
If there's a need to serve other formats (like GIF or SVG), consider adding them to the allowlist. Otherwise, this effectively limits exposure.


41-44: File size limit check is reasonable.
For files above 10MB, or if you anticipate streaming large assets in the future, consider switching to streaming endpoints instead of pulling the entire file in memory.


46-55: Consider adding ETag or Last-Modified headers.
With a full year (max-age=31536000) cache, any updated file might not be fetched correctly without a versioned filename or additional headers.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cc0e6b4 and 238d3a4.

📒 Files selected for processing (4)
  • backend/src/user/user.model.ts (1 hunks)
  • frontend/src/app/api/media/[...path]/route.ts (1 hunks)
  • frontend/src/graphql/schema.gql (4 hunks)
  • frontend/src/graphql/type.tsx (13 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • backend/src/user/user.model.ts
🔇 Additional comments (19)
frontend/src/graphql/schema.gql (4)

5-9: Well-structured type definition for avatar uploads.

The AvatarUploadResponse type is clearly defined with appropriate required fields for returning avatar upload results.


130-130: Clean implementation of the avatar upload mutation.

The uploadAvatar mutation correctly leverages the existing Upload scalar type and returns a well-defined response type.


187-187: Appropriate query definition for avatar retrieval.

The getUserAvatar query is properly defined with a required userId parameter and returns a nullable String, which is suitable since not all users may have avatars.


239-239: User type properly extended with avatar support.

The optional avatarUrl field is correctly added to the User type with an appropriate nullable type.

frontend/src/graphql/type.tsx (11)

43-47: Consider making avatarUrl optional for the new response type.
If there's any chance that uploading an avatar might fail or return no URL, having a required avatarUrl here could pose risks of null returns at runtime. Confirm whether the server always guarantees a valid URL.


175-175: Mutation field looks good.
The new uploadAvatar field in the Mutation type aligns well with your goal. Please ensure thorough testing for scenarios where the user lacks authorization or the file upload fails.


247-249: Validate authorization checks on upload.
Ensure that only authenticated and authorized users can access the uploadAvatar mutation. This helps prevent unauthorized avatar uploads.


307-307: Query for getUserAvatar is fine.
Returning Maybe<String> is consistent for scenarios where the avatar might not exist.


335-337: User ID required for getUserAvatar.
Confirm that you have security measures in place so that users don’t retrieve confidential data about others.


510-510: New AvatarUploadResponse in ResolversTypes.
No issues with adding this mapping.


550-550: Parent type mapping is correct.
No concerns here.


838-843: Confirm error handling in uploadAvatar resolver.
Verify that your resolver gracefully handles file upload failures, invalid file formats, or missing authentication.


960-965: getUserAvatar resolver definition looks correct.
The return type accommodates users without avatars. Properly handle the null case in the frontend if the user has no avatar.


1020-1024: Optional avatarUrl in User type is appropriate.
This aligns with the possibility of users having no avatar. No issues here.


1053-1053: Resolver registration for AvatarUploadResponse.
No problems adding the new type to the resolvers map. Everything is consistent.

frontend/src/app/api/media/[...path]/route.ts (4)

1-5: Imports look good.
No concerns with the imported modules and usage of the promises API for fs.


6-9: Validate non-empty param path array.
Consider checking if params.path is empty or null to ensure you don't accidentally target the media directory root.


33-39: Robust file existence handling.
This try/catch pattern properly returns a 404 if stat fails. If you want to prevent serving file system symlinks, you could verify fileStat.isSymbolicLink() to reject them.


56-63: Error handling is effectively sanitized.
Logging the detailed error in development while returning a generic message in production is a good practice. For further security, you can omit or redact overly sensitive data from console.error even in development if needed.

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

🧹 Nitpick comments (3)
frontend/src/components/avatar-uploader.tsx (3)

96-107: Enhance error handling robustness.

The implementation has improved error handling but could be more robust when accessing potentially undefined properties.

// Extract the error message if available
let errorMessage = 'Failed to upload avatar';
- if (error.graphQLErrors && error.graphQLErrors.length > 0) {
+ if (error?.graphQLErrors?.length > 0) {
  errorMessage = error.graphQLErrors[0].message;
}

Also consider adding type information to the error parameter:

- } catch (error) {
+ } catch (error: any) {
  console.error('Error uploading avatar:', error);

117-147: UI implementation could be simplified to reduce redundant click handlers.

The component has overlapping click handlers that perform the same action.

The button wrapping the entire component and the Avatar component both have onClick handlers calling triggerFileInput. Consider simplifying:

<button
  onClick={triggerFileInput}
  disabled={loading}
  className="flex justify-end"
>
  <input
    type="file"
    ref={fileInputRef}
    onChange={handleFileChange}
    accept="image/*"
    className="hidden"
  />
  <div className="relative group">
    <Avatar
      className="w-24 h-24 cursor-pointer transition-opacity hover:opacity-80"
-     onClick={triggerFileInput}
    >
      <AvatarImage src={displayUrl} alt="User Avatar" />
      <AvatarFallback>{avatarFallback}</AvatarFallback>
    </Avatar>
    <!-- rest of the component remains the same -->

117-147: Consider adding aria-label for improved accessibility.

The button lacks an accessible label for screen readers.

<button
  onClick={triggerFileInput}
  disabled={loading}
  className="flex justify-end"
+ aria-label="Upload avatar"
>
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 39f04c6 and 51dfd57.

📒 Files selected for processing (9)
  • frontend/src/app/(main)/settings/page.tsx (1 hunks)
  • frontend/src/components/avatar-uploader.tsx (1 hunks)
  • frontend/src/components/chat/chat-panel.tsx (0 hunks)
  • frontend/src/components/chat/index.tsx (3 hunks)
  • frontend/src/components/detail-settings.tsx (2 hunks)
  • frontend/src/components/file-sidebar.tsx (2 hunks)
  • frontend/src/components/settings/settings.tsx (3 hunks)
  • frontend/src/components/sidebar.tsx (2 hunks)
  • frontend/src/components/user-settings-bar.tsx (1 hunks)
💤 Files with no reviewable changes (1)
  • frontend/src/components/chat/chat-panel.tsx
✅ Files skipped from review due to trivial changes (2)
  • frontend/src/app/(main)/settings/page.tsx
  • frontend/src/components/file-sidebar.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • frontend/src/components/chat/index.tsx
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: Install and Build Frontend
🔇 Additional comments (21)
frontend/src/components/detail-settings.tsx (2)

21-21: Component Import Updated

The import has been changed from EditUsernameForm to UserSetting, which reflects the renamed and enhanced component from the settings module.


36-36: Component Usage Updated

The component has been updated from <EditUsernameForm /> to <UserSetting /> to match the renamed component, which now includes additional user setting capabilities like avatar management.

frontend/src/components/sidebar.tsx (2)

7-7: Import Updated for Enhanced User Settings

The import has been changed from UserSettings to UserSettingsBar to use the new component that includes avatar display capabilities.


234-234: User Settings Component Replaced

The UserSettings component has been replaced with UserSettingsBar while maintaining the same prop interface, allowing for a seamless integration of avatar functionality in the sidebar.

frontend/src/components/user-settings-bar.tsx (3)

24-47: Well-implemented URL normalization utility

The normalizeAvatarUrl function properly handles different avatar URL formats, including absolute URLs, relative media paths, and paths containing 'avatars/' segments.


53-153: Well-structured user settings bar component

The UserSettingsBar component follows React best practices with:

  • Properly memoized values and callbacks
  • Clean separation of concerns
  • Responsive design with isSimple prop support
  • Proper avatar fallback handling
  • Normalized avatar URL implementation

The component efficiently integrates with the authentication context and provides a clean interface for navigating to settings and logging out.


155-155: Appropriate use of memoization

The component is correctly wrapped in memo to prevent unnecessary re-renders, optimizing performance.

frontend/src/components/settings/settings.tsx (6)

16-22: Updated imports to support avatar functionality

The import paths have been adjusted and new imports added for avatar management functionality, including the AvatarUploader component and authentication context.


58-58: Component renamed for better semantic meaning

The component has been appropriately renamed from EditUsernameForm to UserSetting to better reflect its expanded functionality, which now includes avatar management.


60-61: Enhanced state management with authentication context

The component now properly uses useAuthContext to access user data and adds state for managing avatar URLs.


68-73: Improved user data handling

The component now correctly initializes both the username and avatar URL from the authenticated user data, replacing the previous localStorage approach.


94-97: Added handler for avatar changes

The new handleAvatarChange function properly updates the avatar URL state when the user uploads a new avatar.


108-114: Integration of avatar uploader component

The AvatarUploader component is well-integrated with appropriate props for the current avatar URL, fallback display, and change handler.

frontend/src/components/avatar-uploader.tsx (8)

1-9: Appropriate imports for an avatar upload component.

The imports cover all necessary dependencies for file handling, UI components, GraphQL mutations, and notifications.


11-34: Well-implemented URL normalization helper.

The normalizeAvatarUrl function handles various avatar URL formats comprehensively:

  • Handles null/undefined values
  • Preserves absolute URLs (http/https)
  • Converts relative media paths to API route paths
  • Handles paths with "avatars/" but missing the "media/" prefix

This ensures consistent avatar URL rendering across different storage scenarios.


36-40: Clear and well-defined component props interface.

The interface AvatarUploaderProps provides a clean contract for the component with appropriate types for each prop.


42-50: Proper usage of React hooks for component state.

The component correctly uses:

  • useMutation for GraphQL operations
  • useRef for accessing the file input DOM element
  • useState for managing preview URL state
  • useAuthContext for accessing user refresh functionality

52-67: Good client-side file validation.

The implementation includes appropriate client-side validation for:

  • File existence check
  • Maximum file size restriction (5MB)
  • Allowed file types (JPEG, PNG, WebP)

This prevents unnecessary server calls for invalid files and provides immediate user feedback.


69-74: Add error handling for FileReader operations.

The FileReader operations lack error handling, which could lead to unhandled exceptions if file reading fails.

Add error handling for the FileReader:

// Create a preview URL
const reader = new FileReader();
reader.onload = () => {
  setPreviewUrl(reader.result as string);
};
+ reader.onerror = () => {
+   toast.error('Failed to read file');
+   console.error('FileReader error:', reader.error);
+ };
reader.readAsDataURL(file);

76-95: Well-structured avatar upload flow with user feedback.

The implementation correctly:

  • Uses Apollo client's file upload capabilities
  • Sets necessary headers for file uploads
  • Handles successful uploads with proper user notification
  • Updates the parent component via callback
  • Refreshes user information to ensure consistency

This provides a complete upload flow with appropriate user feedback.


110-116: Clean implementation of file input trigger and URL display logic.

The implementation cleanly handles:

  • Programmatic triggering of the file input
  • Prioritizing preview URL over normalized current avatar URL

This creates a smooth user experience for avatar selection and preview.

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

🧹 Nitpick comments (2)
frontend/src/components/avatar-uploader.tsx (2)

119-149: Enhance accessibility of the avatar upload UI.

The UI implementation has good visual feedback with hover effects and loading states, but could benefit from accessibility improvements.

Consider these accessibility enhancements:

 <button
   onClick={triggerFileInput}
   disabled={loading}
-  className="flex justify-end"
+  className="flex justify-end"
+  aria-label="Upload avatar image"
+  title="Click to upload a new avatar image (Max: 5MB, Formats: JPEG, PNG, WebP)"
 >

This provides users with clear instructions about file requirements before they attempt to upload.


120-147: Consider adding image optimization before upload.

The current implementation uploads the original image without any optimization, which might result in unnecessarily large files.

Consider adding client-side image resizing before upload for better performance:

const optimizeImage = async (file: File, maxWidth = 512, maxHeight = 512): Promise<Blob> => {
  return new Promise((resolve, reject) => {
    const img = new Image();
    img.onload = () => {
      const canvas = document.createElement('canvas');
      let width = img.width;
      let height = img.height;
      
      // Calculate new dimensions while maintaining aspect ratio
      if (width > height) {
        if (width > maxWidth) {
          height = Math.round((height * maxWidth) / width);
          width = maxWidth;
        }
      } else {
        if (height > maxHeight) {
          width = Math.round((width * maxHeight) / height);
          height = maxHeight;
        }
      }
      
      canvas.width = width;
      canvas.height = height;
      const ctx = canvas.getContext('2d');
      ctx?.drawImage(img, 0, 0, width, height);
      
      // Convert to blob
      canvas.toBlob(
        (blob) => {
          if (blob) resolve(blob);
          else reject(new Error('Canvas to Blob conversion failed'));
        },
        file.type,
        0.85 // Quality parameter for JPEG
      );
    };
    
    img.onerror = reject;
    img.src = URL.createObjectURL(file);
  });
};

// Then in your handleFileChange function:
const optimizedFile = await optimizeImage(file);
// Use optimizedFile instead of file for upload

This would significantly reduce upload size and improve performance, especially for users with slower internet connections.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 51dfd57 and 20b460d.

📒 Files selected for processing (2)
  • frontend/src/components/avatar-uploader.tsx (1 hunks)
  • frontend/src/components/user-settings-bar.tsx (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • frontend/src/components/user-settings-bar.tsx
🔇 Additional comments (8)
frontend/src/components/avatar-uploader.tsx (8)

11-36: Well-structured URL normalization helper.

The normalizeAvatarUrl function effectively handles various avatar URL formats with appropriate comments explaining each case. The implementation covers absolute URLs, relative media paths, and paths without the media prefix.


38-42: Clean, well-defined interface.

The props interface is clearly defined with appropriate types for all necessary properties required by the component.


59-62: Good client-side validation for file size.

The 5MB file size limit is appropriate for avatar images and helps prevent unnecessarily large uploads. The user gets immediate feedback via toast notification.


65-69: Appropriate file type validation.

Limiting uploads to JPEG, PNG, and WebP formats is a good practice for avatar images. These formats provide a good balance of quality and file size.


71-76: Add error handling for FileReader operations.

The FileReader operations lack error handling, which could lead to unhandled exceptions if file reading fails.


78-98: Good implementation of the avatar upload process with proper feedback.

The avatar upload implementation:

  1. Uses Apollo mutation with proper context for file uploads
  2. Provides success notification to the user
  3. Correctly updates the avatar URL and refreshes user information
  4. Has appropriate error handling

This ensures a smooth user experience during the upload process.


103-105: Add null check before accessing error.graphQLErrors.

Accessing properties without null checks could lead to runtime errors.


1-151: Effective implementation of avatar upload functionality.

The overall component is well-structured with proper error handling, validation, and user feedback. The component:

  1. Validates file size and type before upload
  2. Provides visual feedback during the upload process
  3. Handles various URL formats through the normalization helper
  4. Integrates with authentication context to refresh user information

The implementation follows React best practices with proper separation of concerns and componentization.

@Sma1lboy Sma1lboy merged commit de72da7 into main Mar 12, 2025
5 of 6 checks passed
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.

2 participants