Skip to content

Fix/web image upload cropper#339

Merged
onerandomdevv merged 3 commits into
devfrom
fix/web-image-upload-cropper
May 26, 2026
Merged

Fix/web image upload cropper#339
onerandomdevv merged 3 commits into
devfrom
fix/web-image-upload-cropper

Conversation

@onerandomdevv
Copy link
Copy Markdown
Collaborator

@onerandomdevv onerandomdevv commented May 26, 2026

What does this PR do?

This PR fixes profile/store image upload behavior and adds a reusable image cropping flow for Twizrr profile and store images. Users can now crop user profile photos, store logos, and store banner images before upload, preview them locally, upload through the backend /upload/image endpoint, and persist the returned image URL when saving profile/store settings.

Type of change

  • New feature
  • Bug fix
  • Refactor / cleanup
  • Database migration included
  • Chore / maintenance
  • Documentation

Area affected

  • Backend
  • Web
  • WhatsApp
  • Shared package
  • Database / Prisma
  • GitHub / CI / infrastructure

How to test this

  1. Checkout the branch and run web checks:

    git checkout fix/web-image-upload-cropper
    cd apps/web
    pnpm install
    pnpm run lint
    npx tsc --noEmit
    pnpm run build```
    
  2. Start the app:
    pnpm run dev

  3. Smoke-test image flows:
    /buyer/settings
    /store/settings

Expected result: selecting a profile/store image opens a cropper, cropped previews appear correctly, uploads go through the backend /upload/image endpoint, and the returned image URL is persisted only when the user saves settings.

Pre-commit checklist

  • Backend lint/type/build pass when backend is affected
  • Web lint/type/build pass when web is affected
  • Shared package build passes when shared is affected
  • No console.log left in production code
  • No secrets or .env files committed
  • No new any types added
  • No non-MVP legacy features reintroduced
  • All money values are BigInt kobo, never float
  • Paystack webhook changes verify HMAC before processing
  • Database migrations are Prisma migrations, not db push
  • Database migrations are backward-compatible or risk is documented

Screenshots

Notes for reviewer

Upload behavior:

Uploads still go through backend:
POST /upload/image
No direct frontend Cloudinary uploads
No Cloudinary secrets exposed to frontend
Cropped image is converted before upload
Local preview updates before saving
Final persisted URL is saved through existing profile/store settings update flow

Bug root cause:

The previous frontend behavior uploaded images immediately on file select but did not reliably persist the returned upload URL to PUT /users/me or PUT /stores/me during Save.
This made the upload flow feel like profile/store images were lost or not updating.
This PR keeps the cropped/uploaded image URL in form state and persists it during Save.

Summary by CodeRabbit

  • New Features

    • Added client-side image cropping modal for avatar uploads with zoom and confirmation controls.
    • Introduced image cropping workflow for store logos and banners before upload.
    • Enhanced image file validation with improved error messaging.
  • Improvements

    • Better detection and recovery when image uploads fail to persist to profile.
    • Improved resource cleanup for image previews.

Review Change Stack

@codesandbox
Copy link
Copy Markdown

codesandbox Bot commented May 26, 2026

Review or Edit in CodeSandbox

Open the branch in Web EditorVS CodeInsiders

Open Preview

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 26, 2026

Warning

Review limit reached

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

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

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

⌛ 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: b58c6398-ffd6-4599-bfdb-1322e26f6232

📥 Commits

Reviewing files that changed from the base of the PR and between 5d2a8ea and 7177f37.

📒 Files selected for processing (4)
  • apps/web/src/app/(store)/store/settings/StoreSettingsClient.tsx
  • apps/web/src/components/image/ImageCropperModal.tsx
  • apps/web/src/components/store-settings/StoreImageUpload.tsx
  • apps/web/src/lib/image-crop.ts
📝 Walkthrough

Walkthrough

This PR introduces client-side image cropping utilities and a reusable modal component, refactors avatar and store image upload flows from immediate uploads to draft-based workflows, improves upload response parsing to handle multiple URL field names, and integrates the new cropping workflows into shopper and store settings screens.

Changes

Image cropping and draft-based uploads

Layer / File(s) Summary
Core image cropping library and dependency
apps/web/package.json, apps/web/src/lib/image-crop.ts
Adds react-easy-crop dependency; exports PixelCrop and CroppedImageResult types, MIME-type validation constants, and core utilities: validateUploadImageFile (MIME type and size checks), revokeObjectUrl (cleanup helper), createCroppedImageFile (loads image, crops to canvas, converts to WebP, returns File + preview URL), plus internal helpers for image loading, canvas-to-blob conversion, and filename normalization.
Image cropper modal component
apps/web/src/components/image/ImageCropperModal.tsx
Introduces ImageCropperModal component that wraps react-easy-crop with zoom slider, processing and error states; on confirm calls createCroppedImageFile with pixel crop and output dimensions, routes success to onConfirm callback, and displays error messages. Modal closes only when not processing. Exports CropShape type (circle | squircle | banner).
Upload response parsing improvements
apps/web/src/lib/store-settings.ts, apps/web/src/lib/user.ts
Extends RawUploadResponse types to recognize multiple URL fields (url, secureUrl, imageUrl) and alternate public ID sources (publicId); refactors validation to extract a canonical URL via readUploadUrl helper and normalize via normalizeUploadResult; updates uploadStoreImage and uploadAvatar to use flexible parsing and throw INVALID_RESPONSE on normalization failure; makes UploadAvatarResult.cloudinaryPublicId nullable.
Store image upload component refactor
apps/web/src/components/store-settings/StoreImageUpload.tsx
Removes server-side upload logic; now validates file locally via validateUploadImageFile, stores crop source as object URL, and uses ImageCropperModal configured per image type (banner vs logo with different aspect ratios and output sizes). Changes onChange callback to emit StoreImageDraft ({ file, previewUrl }) instead of remote URL; adds optional disabled prop; revokes object URLs on cancel/confirm.
Avatar upload in buyer settings
apps/web/src/app/(shopper)/buyer/settings/SettingsClient.tsx
Adds avatarCropSource and avatarDraft state for draft-based workflow; file picker validates and sets crop source instead of uploading immediately; crop cancel/confirm handlers revoke URLs and update draft; updates "No changes to save" condition to include pending draft; refactors handleSave to upload draft first, merge resulting profilePhotoUrl into payload, call updateMe, then fetchOwnProfile to verify persistence and detect upload-succeeded-but-profile-didn't-save cases (sets avatarError on mismatch); updates avatar button and sticky save bar UI to reflect combined loading state.
Store settings integration
apps/web/src/app/(store)/store/settings/StoreSettingsClient.tsx
Adds logo/banner draft state and dedicated imageError for upload failures; cleanup effect revokes draft preview URLs; extends handleSaveProfile to upload draft images before calling updateStoreProfile, re-fetches persisted store, and verifies that persisted URLs match uploaded URLs (sets imageError and returns early on mismatch); UI disables image uploads during save and displays imageError in an alert block.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

  • coded-devs/twizrr#326: Introduced the initial avatar upload workflow in SettingsClient that this PR refactors to use client-side cropping instead of immediate uploads.

Poem

🐰 A crop modal blooms with zesty precision,
Drafts dance before their grand revision,
URLs flex in the parsing light,
Avatar and banner dreams upload just right! 🎨✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

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.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title "Fix/web image upload cropper" accurately summarizes the main change: fixing web image upload behavior with a new cropping feature.
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.
Description check ✅ Passed The PR description is well-structured, covers all major template sections, explains the fix clearly, includes testing steps, and acknowledges the pre-commit checklist.

✏️ 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 fix/web-image-upload-cropper

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.

@socket-security
Copy link
Copy Markdown

socket-security Bot commented May 26, 2026

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addedsource-map-support@​0.5.219910010083100
Addedrxjs@​7.8.29910010083100
Addedtypescript@​5.9.3100100909590
Addedzod@​4.3.610010010095100

View full report

@socket-security
Copy link
Copy Markdown

socket-security Bot commented May 26, 2026

Warning

Review the following alerts detected in dependencies.

According to your organization's Security Policy, it is recommended to resolve "Warn" alerts. Learn more about Socket for GitHub.

Action Severity Alert  (click "▶" to expand/collapse)
Warn High
Obfuscated code: npm string.prototype.trimend is 90.0% likely obfuscated

Confidence: 0.90

Location: Package overview

From: pnpm-lock.yamlnpm/eslint-config-next@14.1.0npm/string.prototype.trimend@1.0.9

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/string.prototype.trimend@1.0.9. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn High
Obfuscated code: npm tar is 90.0% likely obfuscated

Confidence: 0.90

Location: Package overview

From: pnpm-lock.yamlnpm/bcrypt@5.1.1npm/tar@7.5.13

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/tar@7.5.13. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

View full report

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (1)
apps/web/src/components/store-settings/StoreImageUpload.tsx (1)

24-31: ⚡ Quick win

Rename the boolean prop to isDisabled.

This introduces a new boolean field that doesn't follow the repo naming rule. Keep the native disabled attribute at the DOM boundary, but expose isDisabled from this component API.

As per coding guidelines, "All boolean field names must be prefixed with 'is', 'has', 'can', or 'should' (e.g. isActive, hasVariants)".

Also applies to: 41-41

🤖 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 `@apps/web/src/components/store-settings/StoreImageUpload.tsx` around lines 24
- 31, The component prop named disabled must be renamed to isDisabled in the
Props interface and component API (update interface Props and the
StoreImageUpload component's props destructure) while keeping the native DOM
attribute on the actual input/button by passing disabled={isDisabled} when
rendering; update every occurrence inside StoreImageUpload (and the prop passed
down at the render site around line 41) to use isDisabled, adjust any prop
forwarding, TypeScript types and call sites inside this file accordingly, and
ensure external usages are updated to the new isDisabled prop name.
🤖 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 `@apps/web/src/app/`(store)/store/settings/StoreSettingsClient.tsx:
- Around line 43-49: The type guard isStoreImageUploadError is too broad (any
Error has a string message) so update it to only match the upload-specific error
shape used by the upload code path: check that value is an object and then
either that v.code is a string AND is one of the known upload error codes (e.g.
'IMAGE_TOO_LARGE', 'INVALID_IMAGE_TYPE', 'IMAGE_UPLOAD_FAILED' — replace with
the actual codes used by the upload API) or that it contains an upload-specific
field (e.g. v.upload?.error or v.uploadError) that your upload routine returns;
change isStoreImageUploadError to use that explicit set/field check so errors
thrown by updateStoreProfile or fetchOwnerStoreFull are not misclassified as
image-upload failures.
- Around line 84-89: The cleanup currently revokes both logoDraft?.previewUrl
and bannerDraft?.previewUrl whenever either changes; change this to revoke only
the preview URL for the draft that actually changed by using two separate
effects (one for logoDraft and one for bannerDraft) or by tracking the previous
previewUrl per draft (e.g., prevLogoRef/prevBannerRef) and revoking only that
value in each effect's cleanup; locate the current useEffect in
StoreSettingsClient.tsx and split it into two useEffect blocks (or add per-draft
prev refs) that call revokeObjectUrl(logoDraft?.previewUrl) only from the logo
effect and revokeObjectUrl(bannerDraft?.previewUrl) only from the banner effect
so an unchanged draft's previewUrl is not revoked.

In `@apps/web/src/components/image/ImageCropperModal.tsx`:
- Around line 43-47: The component currently retains previous session state
(crop, zoom, pixelCrop, error) which can be applied to a new image; update the
component to reset these when the modal input changes by adding an effect that
watches props used to open/replace the image (e.g., imageSrc and open) and on
change calls setCrop({x:0,y:0}), setZoom(1), setPixelCrop(null) and
setError(null); ensure the same reset logic covers any other stateful values
referenced (processing if needed) so the new modal starts with a clean crop
state before onCropComplete runs.

In `@apps/web/src/components/store-settings/StoreImageUpload.tsx`:
- Around line 44-75: cropSource blob URLs are revoked in
handlePick/handleCropCancel/handleCropConfirm but not when the component
unmounts; add a useEffect that watches cropSource and returns a cleanup which
calls revokeObjectUrl(cropSource) so any active URL is revoked on unmount or
when cropSource changes. Update the component to include this effect alongside
the existing state and handlers (cropSource, handlePick, handleCropCancel,
handleCropConfirm) to ensure no blob URLs leak.

In `@apps/web/src/lib/image-crop.ts`:
- Around line 30-31: The validation message is hardcoded to "5MB" while the
check uses the dynamic maxBytes parameter; update the error string produced when
file.size > maxBytes to reflect maxBytes (e.g., compute a human-readable value
from maxBytes like MB) instead of the literal "5MB" so callers with different
limits see the correct size in the message; locate the size check that compares
file.size to maxBytes and replace the static text with a formatted
representation of maxBytes.

---

Nitpick comments:
In `@apps/web/src/components/store-settings/StoreImageUpload.tsx`:
- Around line 24-31: The component prop named disabled must be renamed to
isDisabled in the Props interface and component API (update interface Props and
the StoreImageUpload component's props destructure) while keeping the native DOM
attribute on the actual input/button by passing disabled={isDisabled} when
rendering; update every occurrence inside StoreImageUpload (and the prop passed
down at the render site around line 41) to use isDisabled, adjust any prop
forwarding, TypeScript types and call sites inside this file accordingly, and
ensure external usages are updated to the new isDisabled prop name.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 1fc079e8-6fa4-404c-add6-faca51a4317b

📥 Commits

Reviewing files that changed from the base of the PR and between 5b92d6b and 5d2a8ea.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (8)
  • apps/web/package.json
  • apps/web/src/app/(shopper)/buyer/settings/SettingsClient.tsx
  • apps/web/src/app/(store)/store/settings/StoreSettingsClient.tsx
  • apps/web/src/components/image/ImageCropperModal.tsx
  • apps/web/src/components/store-settings/StoreImageUpload.tsx
  • apps/web/src/lib/image-crop.ts
  • apps/web/src/lib/store-settings.ts
  • apps/web/src/lib/user.ts

Comment thread apps/web/src/app/(store)/store/settings/StoreSettingsClient.tsx
Comment thread apps/web/src/app/(store)/store/settings/StoreSettingsClient.tsx Outdated
Comment thread apps/web/src/components/image/ImageCropperModal.tsx
Comment thread apps/web/src/components/store-settings/StoreImageUpload.tsx
Comment thread apps/web/src/lib/image-crop.ts Outdated
@onerandomdevv onerandomdevv merged commit 0679420 into dev May 26, 2026
8 checks passed
@onerandomdevv onerandomdevv deleted the fix/web-image-upload-cropper branch May 26, 2026 13:49
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.

1 participant