Feat: Add local profile avatar upload and UI integration (#279)#372
Feat: Add local profile avatar upload and UI integration (#279)#372Avnxxh wants to merge 2 commits intoAOSSIE-Org:mainfrom
Conversation
📝 WalkthroughWalkthroughAdds local avatar upload: backend serves Changes
Sequence DiagramsequenceDiagram
participant User
participant Frontend as Frontend UI<br/>(Profile.tsx)
participant Service as Profile Service<br/>(profileService.ts)
participant Backend as Backend<br/>(upload_controller.go)
participant FileSystem as File System<br/>(./uploads/avatars)
User->>Frontend: Click "Upload Avatar"
Frontend->>Frontend: Open file input dialog
User->>Frontend: Select JPG/PNG file
Frontend->>Frontend: Validate file (size ≤ 5MB)
rect rgba(0, 150, 136, 0.5)
Frontend->>Service: uploadAvatar(token, file)
Service->>Service: Attach file to FormData
Service->>Backend: POST /user/upload-avatar (multipart/form-data)
end
rect rgba(63, 81, 181, 0.5)
Backend->>Backend: Enforce size limit & validate extension
Backend->>Backend: Generate UUID filename
Backend->>FileSystem: Save file to ./uploads/avatars/
Backend->>Service: 200 OK + { avatarUrl }
end
rect rgba(76, 175, 80, 0.5)
Service->>Frontend: Return avatarUrl
Frontend->>Frontend: Update avatar URL in state (optimistic)
Frontend->>Service: updateProfile({ avatarUrl })
Service->>Backend: Persist profile update
Backend->>Frontend: 200 OK
User->>Frontend: See updated avatar
end
Estimated Code Review Effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly Related PRs
Suggested Reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@backend/cmd/server/main.go`:
- Around line 69-71: The two os.MkdirAll calls currently ignore errors; replace
them with a single checked call to os.MkdirAll("uploads/avatars", os.ModePerm)
and handle its error immediately (e.g., log the error with context and exit
non‑zero) so the server fails fast on permission/FS issues; update code around
the main startup (where os.MkdirAll is called) to perform the check and call
log.Fatalf or os.Exit(1) with a clear message when the mkdir fails.
In `@backend/controllers/upload_controller.go`:
- Around line 47-49: The code currently hardcodes baseURL
("http://localhost:1313") when building publicURL, which persists broken links;
update the upload handler to not persist "localhost:1313" by deriving the base
URL from the incoming request or server config instead (e.g., use r.Host and
request scheme or the application's cfg.Server settings) or store and return a
relative path ("/uploads/avatars/%s") so links remain valid across
ports/domains; change the construction of publicURL (which uses baseURL and
newFilename) to use the request-derived host/scheme or a relative URL and ensure
newFilename remains the filename component used to build the path.
In `@frontend/src/Pages/Profile.tsx`:
- Around line 182-218: The file input's onChange handler handleFileUpload does
not reset the hidden file input, so selecting the same file twice won't
retrigger onChange; update handleFileUpload to clear the input value (e.g. reset
e.target.value or use a ref to set input.value = '') after every attempt and
after each early return (validation failure, missing token, success, and catch)
so the input is cleared regardless of outcome; locate handleFileUpload,
uploadAvatar, updateProfile, setErrorMessage, and setSuccessMessage to apply the
reset in all control paths and ensure the file input (the element wired to
onChange) is reset so the same file can be reselected.
- Around line 197-216: The optimistic update sets dashboard.profile.avatarUrl
before ensuring persistence; if uploadAvatar succeeds but updateProfile fails
the UI still shows the new avatar—either move the setDashboard call until after
both uploadAvatar and updateProfile succeed, or capture the previous avatarUrl
(from dashboard.profile.avatarUrl) before updating and in the catch block revert
via setDashboard(...) and call setErrorMessage; specifically update the flow
around uploadAvatar, updateProfile, setDashboard, setSuccessMessage and
setErrorMessage so you restore the prior avatar on failure (or only set the new
avatar after updateProfile returns successfully).
- Around line 816-840: The overlay actions are currently hidden via "opacity-0
group-hover:opacity-100" which makes them inaccessible on touch and keyboard;
change the container's classes to include focus visibility and pointer events
(e.g., replace "opacity-0 group-hover:opacity-100 transition-opacity" with
"opacity-0 group-hover:opacity-100 group-focus-within:opacity-100
transition-opacity pointer-events-none group-hover:pointer-events-auto
group-focus-within:pointer-events-auto") so the controls become visible and
interactive when the avatar container receives focus, ensure the buttons that
call setIsAvatarModalOpen and fileInputRef.current?.click remain native <button>
elements (they already are) so they are tabbable, and add explicit accessible
labels (aria-label or title) to the upload/create buttons and the hidden file
input (and ensure handleFileUpload remains used onChange) so touch and keyboard
users can discover and activate the avatar actions.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: f96d8f8e-b966-4399-ba86-7060cae7cff6
📒 Files selected for processing (5)
backend/cmd/server/main.gobackend/controllers/upload_controller.gobackend/routes/profile.gofrontend/src/Pages/Profile.tsxfrontend/src/services/profileService.ts
frontend/src/Pages/Profile.tsx
Outdated
| <div className="absolute inset-0 bg-black bg-opacity-50 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity gap-3"> | ||
| <button | ||
| type="button" | ||
| onClick={() => setIsAvatarModalOpen(true)} | ||
| title="Create Avatar" | ||
| className="text-white hover:text-primary transition-colors" | ||
| > | ||
| <Pen className="w-5 h-5" /> | ||
| </button> | ||
| <button | ||
| type="button" | ||
| onClick={() => fileInputRef.current?.click()} | ||
| title="Upload Avatar" | ||
| className="text-white hover:text-primary transition-colors" | ||
| > | ||
| <ImageIcon className="w-5 h-5" /> | ||
| </button> | ||
| </div> | ||
| <input | ||
| type="file" | ||
| ref={fileInputRef} | ||
| className="hidden" | ||
| accept=".jpg,.jpeg,.png" | ||
| onChange={handleFileUpload} | ||
| /> |
There was a problem hiding this comment.
Don't make the new avatar actions hover-only.
On touch devices there is no hover, and keyboard users can land on invisible controls. That makes the upload/custom-avatar actions effectively undiscoverable.
Suggested fix
- <div className="absolute inset-0 bg-black bg-opacity-50 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity gap-3">
+ <div className="absolute inset-0 bg-black/50 flex items-center justify-center gap-3 opacity-100 sm:opacity-0 sm:pointer-events-none sm:group-hover:opacity-100 sm:group-hover:pointer-events-auto sm:group-focus-within:opacity-100 sm:group-focus-within:pointer-events-auto transition-opacity">
<button
type="button"
onClick={() => setIsAvatarModalOpen(true)}
title="Create Avatar"
+ aria-label="Create avatar"
className="text-white hover:text-primary transition-colors"
>
<Pen className="w-5 h-5" />
</button>
<button
type="button"
onClick={() => fileInputRef.current?.click()}
title="Upload Avatar"
+ aria-label="Upload avatar"
className="text-white hover:text-primary transition-colors"
>
<ImageIcon className="w-5 h-5" />
</button>
</div>📝 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.
| <div className="absolute inset-0 bg-black bg-opacity-50 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity gap-3"> | |
| <button | |
| type="button" | |
| onClick={() => setIsAvatarModalOpen(true)} | |
| title="Create Avatar" | |
| className="text-white hover:text-primary transition-colors" | |
| > | |
| <Pen className="w-5 h-5" /> | |
| </button> | |
| <button | |
| type="button" | |
| onClick={() => fileInputRef.current?.click()} | |
| title="Upload Avatar" | |
| className="text-white hover:text-primary transition-colors" | |
| > | |
| <ImageIcon className="w-5 h-5" /> | |
| </button> | |
| </div> | |
| <input | |
| type="file" | |
| ref={fileInputRef} | |
| className="hidden" | |
| accept=".jpg,.jpeg,.png" | |
| onChange={handleFileUpload} | |
| /> | |
| <div className="absolute inset-0 bg-black/50 flex items-center justify-center gap-3 opacity-100 sm:opacity-0 sm:pointer-events-none sm:group-hover:opacity-100 sm:group-hover:pointer-events-auto sm:group-focus-within:opacity-100 sm:group-focus-within:pointer-events-auto transition-opacity"> | |
| <button | |
| type="button" | |
| onClick={() => setIsAvatarModalOpen(true)} | |
| title="Create Avatar" | |
| aria-label="Create avatar" | |
| className="text-white hover:text-primary transition-colors" | |
| > | |
| <Pen className="w-5 h-5" /> | |
| </button> | |
| <button | |
| type="button" | |
| onClick={() => fileInputRef.current?.click()} | |
| title="Upload Avatar" | |
| aria-label="Upload avatar" | |
| className="text-white hover:text-primary transition-colors" | |
| > | |
| <ImageIcon className="w-5 h-5" /> | |
| </button> | |
| </div> | |
| <input | |
| type="file" | |
| ref={fileInputRef} | |
| className="hidden" | |
| accept=".jpg,.jpeg,.png" | |
| onChange={handleFileUpload} | |
| /> |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@frontend/src/Pages/Profile.tsx` around lines 816 - 840, The overlay actions
are currently hidden via "opacity-0 group-hover:opacity-100" which makes them
inaccessible on touch and keyboard; change the container's classes to include
focus visibility and pointer events (e.g., replace "opacity-0
group-hover:opacity-100 transition-opacity" with "opacity-0
group-hover:opacity-100 group-focus-within:opacity-100 transition-opacity
pointer-events-none group-hover:pointer-events-auto
group-focus-within:pointer-events-auto") so the controls become visible and
interactive when the avatar container receives focus, ensure the buttons that
call setIsAvatarModalOpen and fileInputRef.current?.click remain native <button>
elements (they already are) so they are tabbable, and add explicit accessible
labels (aria-label or title) to the upload/create buttons and the hidden file
input (and ensure handleFileUpload remains used onChange) so touch and keyboard
users can discover and activate the avatar actions.
There was a problem hiding this comment.
♻️ Duplicate comments (2)
frontend/src/Pages/Profile.tsx (2)
835-854:⚠️ Potential issue | 🟡 MinorAccessibility: Add pointer-events handling for touch and keyboard users.
The overlay now has
focus-within:opacity-100for keyboard visibility, but the buttons remain non-interactive when hidden due to missingpointer-eventsclasses. On touch devices, the hover state never activates, making the buttons undiscoverable.Proposed fix
- <div className="absolute inset-0 bg-black/50 flex items-center justify-center gap-3 opacity-0 group-hover:opacity-100 focus-within:opacity-100 sm:opacity-0 sm:group-hover:opacity-100 transition-opacity"> + <div className="absolute inset-0 bg-black/50 flex items-center justify-center gap-3 opacity-100 pointer-events-auto sm:opacity-0 sm:pointer-events-none sm:group-hover:opacity-100 sm:group-hover:pointer-events-auto sm:group-focus-within:opacity-100 sm:group-focus-within:pointer-events-auto transition-opacity">🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/Pages/Profile.tsx` around lines 835 - 854, The overlay div currently becomes visible via opacity but its children remain non-interactive; update the overlay's className to toggle pointer events alongside opacity (use pointer-events-none when hidden and pointer-events-auto when visible) by adding the Tailwind variants for group-hover:pointer-events-auto and focus-within:pointer-events-auto (and keep pointer-events-none as the default/for sm breakpoint where needed) so the buttons (onClick handlers setIsAvatarModalOpen and fileInputRef.current?.click) are reachable by keyboard and touch users.
182-237:⚠️ Potential issue | 🟡 MinorCapture
previousAvatarUrlbefore the upload call, not after.On line 203,
previousAvatarUrlis captured afteruploadAvatar()succeeds. IfuploadAvatar()throws, the outer catch block (lines 232-233) executes butpreviousAvatarUrlis never assigned, so no revert is possible. Move the capture before the try block.Proposed fix
const handleFileUpload = async (e: React.ChangeEvent<HTMLInputElement>) => { const input = e.currentTarget; const file = input.files?.[0]; if (!file) return; + const previousAvatarUrl = dashboard?.profile.avatarUrl; if (file.size > 5 * 1024 * 1024) { setErrorMessage("File size should not exceed 5MB."); @@ -200,7 +201,6 @@ try { const response = await uploadAvatar(token, file); const newAvatarUrl = response.avatarUrl; - const previousAvatarUrl = dashboard?.profile.avatarUrl; setDashboard((current: DashboardData | null) => current ? {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/Pages/Profile.tsx` around lines 182 - 237, In handleFileUpload, previousAvatarUrl is captured too late (after uploadAvatar) so if uploadAvatar throws you cannot revert; move the assignment const previousAvatarUrl = dashboard?.profile.avatarUrl to before the try that calls uploadAvatar (capture the current dashboard.profile avatarUrl immediately after validating token and file), then use that captured previousAvatarUrl in the inner catch to restore state after a failed updateProfile; ensure you still handle dashboard possibly being null when capturing.
🧹 Nitpick comments (1)
backend/cmd/server/main.go (1)
86-87: Static file serving looks correct, but consider placement relative to CORS.The static route is registered before the CORS middleware. If frontend JavaScript needs to fetch avatars via
fetch()with credentials or custom headers, CORS preflight may fail. Currently this works because<img>tags don't trigger CORS, but if you ever need programmatic access from the frontend, you may need to move this after CORS middleware.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/cmd/server/main.go` around lines 86 - 87, The static route registration router.Static("/uploads", "./uploads") is placed before the CORS middleware and can cause CORS preflight failures for programmatic frontend fetches; move this router.Static call so it is registered after the CORS middleware registration (i.e., after the router.Use(...) call that sets up CORS) so the uploads route is covered by the CORS rules.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In `@frontend/src/Pages/Profile.tsx`:
- Around line 835-854: The overlay div currently becomes visible via opacity but
its children remain non-interactive; update the overlay's className to toggle
pointer events alongside opacity (use pointer-events-none when hidden and
pointer-events-auto when visible) by adding the Tailwind variants for
group-hover:pointer-events-auto and focus-within:pointer-events-auto (and keep
pointer-events-none as the default/for sm breakpoint where needed) so the
buttons (onClick handlers setIsAvatarModalOpen and fileInputRef.current?.click)
are reachable by keyboard and touch users.
- Around line 182-237: In handleFileUpload, previousAvatarUrl is captured too
late (after uploadAvatar) so if uploadAvatar throws you cannot revert; move the
assignment const previousAvatarUrl = dashboard?.profile.avatarUrl to before the
try that calls uploadAvatar (capture the current dashboard.profile avatarUrl
immediately after validating token and file), then use that captured
previousAvatarUrl in the inner catch to restore state after a failed
updateProfile; ensure you still handle dashboard possibly being null when
capturing.
---
Nitpick comments:
In `@backend/cmd/server/main.go`:
- Around line 86-87: The static route registration router.Static("/uploads",
"./uploads") is placed before the CORS middleware and can cause CORS preflight
failures for programmatic frontend fetches; move this router.Static call so it
is registered after the CORS middleware registration (i.e., after the
router.Use(...) call that sets up CORS) so the uploads route is covered by the
CORS rules.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: e05e1e87-7140-4b38-b3ae-64fc111848bd
📒 Files selected for processing (3)
backend/cmd/server/main.gobackend/controllers/upload_controller.gofrontend/src/Pages/Profile.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
- backend/controllers/upload_controller.go
Addressed Issues:
Fixes #279
Additional Notes:
This PR implements the **Profile Avatar Upload Feature (Local File Support) **, allowing users to upload custom profile images from their local device.
🚀 Features Implemented
🔧 Backend
upload_controller.goUploadAvatarhandler:.jpg,.jpeg,.png)./uploads/avatars/main.go:/uploads/user/upload-avatar🌐 Frontend Service
uploadAvatar(token, file)inprofileService.tsFormDatafor file upload🎨 UI Enhancements
Profile.tsx🎯 Result
🧪 How to Test
Backend:
/user/upload-avataruploads/avatars/Frontend:
AI Usage Disclosure:
I have used the following AI models and tools:
Checklist
Summary by CodeRabbit
New Features
Improvements