Skip to content

Image uploads handling - #81

Merged
hnam10 merged 9 commits into
mainfrom
image-uploads-handling
Jun 13, 2026
Merged

Image uploads handling#81
hnam10 merged 9 commits into
mainfrom
image-uploads-handling

Conversation

@hnam10

@hnam10 hnam10 commented Jun 13, 2026

Copy link
Copy Markdown
Collaborator

Summary by CodeRabbit

  • New Features
    • Added image upload functionality with support for JPEG, PNG, and WebP formats.
    • Images are automatically compressed before upload to optimize performance.
    • Users can upload up to 3 images with real-time preview and status indicators.
    • Enforced 5MB maximum file size limit per image with validation feedback.

@coderabbitai

coderabbitai Bot commented Jun 13, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@hnam10, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 10 minutes and 47 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more credits in the billing tab to continue.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 05f09708-de78-479b-916d-4f898ecf62b0

📥 Commits

Reviewing files that changed from the base of the PR and between 63e0c93 and 0c4d884.

⛔ Files ignored due to path filters (1)
  • backend/pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (8)
  • backend/package.json
  • backend/src/lib/r2.ts
  • backend/src/routes/uploads.ts
  • backend/src/types/uploads.ts
  • foundit-ui/components/uploadImage.tsx
  • foundit-ui/hooks/useImageUploadGallery.ts
  • foundit-ui/types/uploads.ts
  • foundit-ui/utils/handleImageUpload.ts
📝 Walkthrough

Walkthrough

This PR introduces a complete image upload workflow using Cloudflare R2 presigned URLs. It adds backend infrastructure for presigned-URL generation, frontend image compression, and a React component for image selection and upload management.

Changes

Image Upload with Presigned URLs

Layer / File(s) Summary
R2 Storage Configuration and Dependencies
backend/.env.example, backend/package.json, backend/src/lib/r2.ts, backend/src/types/uploads.ts
Environment variables for R2 credentials and endpoint, AWS SDK S3 client dependencies pinned to ^3.1065.0, initialized S3Client configured for R2, and PresignedUrlResponse type defining presigned-URL response schema.
Backend Presigned URL Endpoint
backend/src/routes/uploads.ts, backend/src/index.ts
POST /api/uploads/presigned-url handler validates image metadata (JPEG/PNG/WebP, max 5MB), generates unique R2 object keys under reports/, creates presigned upload commands with 5-hour expiry, and returns signed URLs alongside image metadata; route mounted in main Express app.
Frontend Image Compression and Upload
foundit-ui/utils/imageCompression.ts, foundit-ui/utils/handleImageUpload.ts, foundit-ui/package.json
Two-pass image compression enforcing 5MB size limit using browser-image-compression library, presigned-URL request handler that fetches backend-signed URLs and performs direct PUT uploads to R2, and dependency declaration for browser-image-compression@^2.0.2.
Frontend Image Upload Component and Hook
foundit-ui/hooks/useImageUploadGallery.ts, foundit-ui/components/uploadImage.tsx
useImageUploadGallery hook manages up to 3 image slots with per-image upload state tracking, preview URL generation, and removal with cleanup; ImageUploadGallery Chakra UI component renders file input (JPEG/PNG/WebP), thumbnail grid with uploading/error overlays, remove buttons per image, and size/format help text.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant Gallery as ImageUploadGallery
  participant Hook as useImageUploadGallery
  participant Compress as compressImage
  participant Handler as handleImageUpload
  participant API as POST /presigned-url
  participant R2
  User->>Gallery: Select image(s)
  Gallery->>Hook: handleFilesSelected(files)
  Hook->>Compress: Compress each file
  Compress->>Hook: Compressed File
  Hook->>Handler: (accessToken, compressed file)
  Handler->>API: Request presigned URL
  API->>Handler: uploadUrl, imageUrl, fileType
  Handler->>R2: PUT compressed file
  R2->>Handler: Success
  Handler->>Hook: imageUrl, fileType, fileSizeKb
  Hook->>Gallery: Update image state to done
  Gallery->>User: Display thumbnail + success
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 Upload images with flair,
Compress, presign, and send them there!
R2 buckets hold them tight,
Five bright hours, upload's just right.
Click to choose, watch it flow—
Gallery's ready, off we go!

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Image uploads handling' is vague and generic, using non-descriptive language that could apply to many different implementation approaches without clarifying the specific technical changes or scope. Consider a more specific title that describes the main change, such as 'Add Cloudflare R2 presigned URL uploads with image compression' or 'Implement image upload endpoint with R2 backend storage'.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch image-uploads-handling

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 10

🧹 Nitpick comments (3)
foundit-ui/components/uploadImage.tsx (1)

1-30: 💤 Low value

Unused hook return values.

The hook returns inputRef (line 29) and hasImages, but neither is used in this component. inputRef is re-declared internally, and images.length > 0 is used instead of hasImages.

Consider removing unused destructured values or utilizing them:

-  const { images, canAddMore, maxImages, handleFilesSelected, handleRemove } =
+  const { images, inputRef, canAddMore, hasImages, maxImages, handleFilesSelected, handleRemove } =
     useImageUploadGallery({ accessToken, onChange });

Then use inputRef with the input element and replace images.length > 0 with hasImages on line 67.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@foundit-ui/components/uploadImage.tsx` around lines 1 - 30, The component
currently destructures inputRef and hasImages from useImageUploadGallery but
doesn't use them (and re-declares inputRef internally) — update
ImageUploadGallery to either stop destructuring them or actually use them:
remove the internal redeclaration of inputRef, attach the hook-provided inputRef
to the file input element used by handleFilesSelected, and replace any checks of
images.length > 0 with hasImages; keep handleFilesSelected and handleRemove
as-is to wire selection/removal to the hook.
foundit-ui/hooks/useImageUploadGallery.ts (1)

94-102: ⚖️ Poor tradeoff

Consider canceling in-flight uploads when removing images.

When a user removes an image with status === 'uploading', the upload continues in the background, wasting bandwidth and potentially causing a state update on an unmounted component.

You could refactor to use AbortController to cancel the fetch requests in handleImageUpload, though this adds complexity for a relatively minor optimization.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@foundit-ui/hooks/useImageUploadGallery.ts` around lines 94 - 102, The remove
handler currently revokes the preview URL but doesn't stop in-flight uploads;
update handleImageUpload to create and attach an AbortController to the image
object (e.g., add an abortController field when starting upload) and modify
handleRemove to detect if the removed image has status === 'uploading' and call
removed.abortController.abort() (and cleanup the controller reference) before
revoking the URL and filtering state; ensure setImages still calls
notifyChange(next) and that any fetch handlers respect the AbortSignal to avoid
state updates after abort.
backend/package.json (1)

13-14: AWS SDK v3.1065.0 is published; ^3.1065.0 will already float to newer 3.x releases

@aws-sdk/client-s3@3.1065.0 and @aws-sdk/s3-request-presigner@3.1065.0 are both published (released June 9, 2026). As of June 13, 2026, the latest @aws-sdk/client-s3 is 3.1067.0; since the dependency uses ^3.1065.0, you’ll already allow updates to later 3.x versions without changing this range (subject to your usual compatibility testing).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/package.json` around lines 13 - 14, The package.json entries for
"`@aws-sdk/client-s3`" and "`@aws-sdk/s3-request-presigner`" use ^3.1065.0 which
already allows newer 3.x releases, so either leave them as-is or explicitly pin;
if you intended to lock to exactly 3.1065.0 remove the caret from both
dependency values (change "^3.1065.0" to "3.1065.0") for "`@aws-sdk/client-s3`"
and "`@aws-sdk/s3-request-presigner`", otherwise revert any accidental change and
keep the caret to continue allowing patch/minor updates.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@backend/src/lib/r2.ts`:
- Around line 3-10: Validate R2 environment variables before creating the
S3Client: check process.env.R2_ENDPOINT, process.env.R2_ACCESS_KEY_ID, and
process.env.R2_SECRET_ACCESS_KEY and fail fast with a clear error (throw or
process.exit(1) after logging) if any are missing, then instantiate the S3Client
(exported as r2) using the validated values; place the validation block
immediately before the new S3Client(...) creation to mirror the JWT secret
startup check pattern found in backend/src/index.ts.

In `@backend/src/routes/uploads.ts`:
- Line 6: The presigned URL endpoint is currently unauthenticated because the
requireAuth import was commented out; re-enable and import requireAuth and apply
it to the presigned URL route (e.g., router.post('/presigned-url', requireAuth,
async (req, res) => { ... })) so the middleware runs before the handler, and
update the handler to use authenticated context (req.user or req.session) to
validate/authorize the upload request and return 401/403 for
unauthenticated/unauthorized callers; ensure the backend consumes the
Authorization header the frontend sends and does not allow unauthenticated
generation of presigned URLs.
- Around line 77-79: The presigned URL expiry is set to 5 hours when calling
getSignedUrl (variable uploadUrl); change the expiresIn value to a much shorter
TTL (e.g., 15 * 60 for 15 minutes) in the getSignedUrl call to limit exposure,
and optionally make this TTL configurable (e.g., UPLOAD_PRESIGNED_TTL) so
handlers using r2 and command can adjust to 30–60 minutes for very large uploads
if needed.
- Around line 59-66: The current size check uses the untrusted fileSizeKb from
req.body (variable fileSizeKb) in the uploads route, so update the presigned
upload generation to enforce size on the server: when creating the presigned
PutObject URL (the code that constructs a PutObjectCommand / calls
getSignedUrl), add a ContentLengthRange condition (ContentLengthRange: [0, 5 *
1024 * 1024]) to the request parameters so the upload is rejected if the client
declares a larger Content-Length; additionally implement a post-upload
validation step in the same uploads route or a new webhook/lifecycle handler to
fetch the uploaded object's actual size (headObject or equivalent) and
delete/reject objects exceeding 5MB to cover clients that spoof headers.
- Line 72: The handler that builds the PutObjectCommand uses
process.env.R2_BUCKET! (seen in uploads.ts) which can be undefined and will
cause getSignedUrl to fail; add centralized environment validation at server
startup to assert R2_BUCKET is present and throw a clear error if missing, then
remove non-null assertions in the upload code (replace process.env.R2_BUCKET!
usage with the validated config value or a Config.getR2Bucket() accessor) so
PutObjectCommand always receives a defined Bucket; ensure the startup validation
runs before any route registration so endpoints like the upload route and
functions constructing PutObjectCommand/getSignedUrl cannot run without a valid
R2_BUCKET.

In `@foundit-ui/components/uploadImage.tsx`:
- Around line 34-64: The Label's text color is fixed to "blue.500" even when
error is true; update the Label's color prop to mirror the other error-based
styles (e.g., color={error ? 'red.500' : 'blue.500'}) so text and icons reflect
the error state consistently; adjust any related hover/_hover styles if needed
to prevent blue hover on error. Use the Label element and its color,
borderColor, bg, and _hover props to implement this change.
- Around line 70-131: Fix the typo in the thumbnail Box width: replace the
invalid w="80%%" with a single-percent value (e.g., w="80%") in the
GridItem/Image preview block (look for GridItem using img.previewUrl, Box that
sets w/h and Image that uses img.previewUrl). Also review the height value
(h="80%") for consistency—consider using equal width/height or an aspectRatio on
the same Box to ensure consistent thumbnails; keep the existing CloseButton
handler handleRemove unchanged.

In `@foundit-ui/hooks/useImageUploadGallery.ts`:
- Around line 50-92: The hook creates object URLs in the file loop (previewUrl
via URL.createObjectURL) but doesn't revoke them on component unmount; add a
React useEffect cleanup in useImageUploadGallery that on unmount iterates the
current images state and calls URL.revokeObjectURL(img.previewUrl) for each
image (and optionally revoke when an image status changes to ensure no leaks),
import useEffect from React, and keep existing handleRemove revocation logic
intact so all created object URLs are revoked either when removed or when the
component unmounts.
- Around line 50-77: The hook currently compresses files via compressImage
before calling handleImageUpload, but handleImageUpload also compresses
internally causing double compression and quality loss; fix by removing internal
compression from handleImageUpload (foundit-ui/utils/handleImageUpload.ts):
change its signature to accept an already-compressed File/Blob, delete the
internal call to compressImage, and ensure it uses the provided file directly
when building the upload payload; update any callers (e.g.,
useImageUploadGallery's call to handleImageUpload and its type expectations) and
adjust types/tests if needed.

In `@foundit-ui/utils/handleImageUpload.ts`:
- Line 2: The file handleImageUpload.ts imports the PresignedUrlResponse type
from the backend src tree which breaks module boundaries; to fix, remove the
cross-repo import and instead define or import a local frontend duplicate of the
interface (e.g., create foundit-ui/types/uploads.ts exporting
PresignedUrlResponse with fields uploadUrl, imageUrl, fileType, fileSizeKb) and
update handleImageUpload.ts to import PresignedUrlResponse from that local
frontend types module so the function(s) that reference PresignedUrlResponse use
the local type rather than ../../backend/src/types/uploads.

---

Nitpick comments:
In `@backend/package.json`:
- Around line 13-14: The package.json entries for "`@aws-sdk/client-s3`" and
"`@aws-sdk/s3-request-presigner`" use ^3.1065.0 which already allows newer 3.x
releases, so either leave them as-is or explicitly pin; if you intended to lock
to exactly 3.1065.0 remove the caret from both dependency values (change
"^3.1065.0" to "3.1065.0") for "`@aws-sdk/client-s3`" and
"`@aws-sdk/s3-request-presigner`", otherwise revert any accidental change and keep
the caret to continue allowing patch/minor updates.

In `@foundit-ui/components/uploadImage.tsx`:
- Around line 1-30: The component currently destructures inputRef and hasImages
from useImageUploadGallery but doesn't use them (and re-declares inputRef
internally) — update ImageUploadGallery to either stop destructuring them or
actually use them: remove the internal redeclaration of inputRef, attach the
hook-provided inputRef to the file input element used by handleFilesSelected,
and replace any checks of images.length > 0 with hasImages; keep
handleFilesSelected and handleRemove as-is to wire selection/removal to the
hook.

In `@foundit-ui/hooks/useImageUploadGallery.ts`:
- Around line 94-102: The remove handler currently revokes the preview URL but
doesn't stop in-flight uploads; update handleImageUpload to create and attach an
AbortController to the image object (e.g., add an abortController field when
starting upload) and modify handleRemove to detect if the removed image has
status === 'uploading' and call removed.abortController.abort() (and cleanup the
controller reference) before revoking the URL and filtering state; ensure
setImages still calls notifyChange(next) and that any fetch handlers respect the
AbortSignal to avoid state updates after abort.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 6ddd6a7e-1dd9-4065-9ba8-04e15afae383

📥 Commits

Reviewing files that changed from the base of the PR and between 929c089 and 63e0c93.

⛔ Files ignored due to path filters (2)
  • backend/pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
  • foundit-ui/pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (11)
  • backend/.env.example
  • backend/package.json
  • backend/src/index.ts
  • backend/src/lib/r2.ts
  • backend/src/routes/uploads.ts
  • backend/src/types/uploads.ts
  • foundit-ui/components/uploadImage.tsx
  • foundit-ui/hooks/useImageUploadGallery.ts
  • foundit-ui/package.json
  • foundit-ui/utils/handleImageUpload.ts
  • foundit-ui/utils/imageCompression.ts

Comment thread backend/src/lib/r2.ts Outdated
Comment thread backend/src/routes/uploads.ts Outdated
Comment thread backend/src/routes/uploads.ts
Comment thread backend/src/routes/uploads.ts Outdated
Comment thread backend/src/routes/uploads.ts
Comment on lines +34 to +64
<Label
htmlFor="image-upload-input"
w="100%"
h="40px"
display="flex"
alignItems="center"
justifyContent="center"
gap={2}
border="1px dashed"
borderColor={error ? 'red.500' : 'blue.300'}
bg={error ? 'red.50' : 'blue.50'}
color="blue.500"
borderRadius="sm"
cursor={canAddMore ? 'pointer' : 'not-allowed'}
opacity={canAddMore ? 1 : 0.6}
fontWeight={600}
transition="background 0.15s ease"
_hover={canAddMore ? { bg: 'blue.100' } : undefined}
>
<LuUpload size={16} />
<Text fontSize="sm">Upload Picture</Text>
<input
id="image-upload-input"
type="file"
accept="image/jpeg,image/png,image/webp"
multiple
hidden
disabled={!canAddMore}
onChange={handleFilesSelected}
/>
</Label>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Text color should be conditional on error state.

Lines 43-44 apply red border and background when error is truthy, but line 45 keeps color="blue.500" regardless. This creates a visual inconsistency.

🎨 Proposed fix
         bg={error ? 'red.50' : 'blue.50'}
-        color="blue.500"
+        color={error ? 'red.500' : 'blue.500'}
         borderRadius="sm"
📝 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
<Label
htmlFor="image-upload-input"
w="100%"
h="40px"
display="flex"
alignItems="center"
justifyContent="center"
gap={2}
border="1px dashed"
borderColor={error ? 'red.500' : 'blue.300'}
bg={error ? 'red.50' : 'blue.50'}
color="blue.500"
borderRadius="sm"
cursor={canAddMore ? 'pointer' : 'not-allowed'}
opacity={canAddMore ? 1 : 0.6}
fontWeight={600}
transition="background 0.15s ease"
_hover={canAddMore ? { bg: 'blue.100' } : undefined}
>
<LuUpload size={16} />
<Text fontSize="sm">Upload Picture</Text>
<input
id="image-upload-input"
type="file"
accept="image/jpeg,image/png,image/webp"
multiple
hidden
disabled={!canAddMore}
onChange={handleFilesSelected}
/>
</Label>
<Label
htmlFor="image-upload-input"
w="100%"
h="40px"
display="flex"
alignItems="center"
justifyContent="center"
gap={2}
border="1px dashed"
borderColor={error ? 'red.500' : 'blue.300'}
bg={error ? 'red.50' : 'blue.50'}
color={error ? 'red.500' : 'blue.500'}
borderRadius="sm"
cursor={canAddMore ? 'pointer' : 'not-allowed'}
opacity={canAddMore ? 1 : 0.6}
fontWeight={600}
transition="background 0.15s ease"
_hover={canAddMore ? { bg: 'blue.100' } : undefined}
>
<LuUpload size={16} />
<Text fontSize="sm">Upload Picture</Text>
<input
id="image-upload-input"
type="file"
accept="image/jpeg,image/png,image/webp"
multiple
hidden
disabled={!canAddMore}
onChange={handleFilesSelected}
/>
</Label>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@foundit-ui/components/uploadImage.tsx` around lines 34 - 64, The Label's text
color is fixed to "blue.500" even when error is true; update the Label's color
prop to mirror the other error-based styles (e.g., color={error ? 'red.500' :
'blue.500'}) so text and icons reflect the error state consistently; adjust any
related hover/_hover styles if needed to prevent blue hover on error. Use the
Label element and its color, borderColor, bg, and _hover props to implement this
change.

Comment thread foundit-ui/components/uploadImage.tsx
Comment thread foundit-ui/hooks/useImageUploadGallery.ts
Comment thread foundit-ui/hooks/useImageUploadGallery.ts
Comment thread foundit-ui/utils/handleImageUpload.ts Outdated
@hnam10
hnam10 merged commit 5480556 into main Jun 13, 2026
4 checks passed
@hnam10
hnam10 deleted the image-uploads-handling branch June 13, 2026 22:21
@coderabbitai coderabbitai Bot mentioned this pull request Jun 15, 2026
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