Skip to content

Joel/forms revamp#106

Merged
joelchem merged 15 commits into
mainfrom
joel/forms-revamp
May 25, 2026
Merged

Joel/forms revamp#106
joelchem merged 15 commits into
mainfrom
joel/forms-revamp

Conversation

@joelchem
Copy link
Copy Markdown
Collaborator

@joelchem joelchem commented May 25, 2026

its all the commit names

Summary by CodeRabbit

  • New Features

    • Photo carousel for multiple donation photos
    • Inventory category deletion with confirmation
    • Preferred language field shown for non-English clients
  • Bug Fixes

    • Client email now optional
    • ZIP code validation strictly enforces 5 digits
    • Donation item validation blocks progression when fields/photos missing
  • Improvements

    • Stronger form validation, clearer inline errors, and improved scrolling to errors
    • Apt/Unit field required and preserved across edits
    • Improved photo upload dropzone and UI tweaks across forms and listings

Review Change Stack

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 25, 2026

Warning

Review limit reached

@joelchem, we couldn't start this review because you've used your available PR reviews for now.

Your plan includes 1 review of capacity. Refill in 52 minutes and 45 seconds.

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

⌛ How to resolve this issue?

After more review capacity refills, 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 have higher rate limits than trial, open-source, and free plans. In all cases, review capacity refills continuously over time.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 01576108-8ae4-4821-b1ca-44198a76df9b

📥 Commits

Reviewing files that changed from the base of the PR and between 3b6a1f8 and 7a12232.

📒 Files selected for processing (2)
  • components/client-requests/RequestDetails.tsx
  • components/form/PhotoDropzone.tsx

Walkthrough

This pull request enhances the client request and donation forms with stricter validation, required apartment fields, and optional email support. It introduces a new photo upload component, removes item size tracking, and adds inventory category deletion. Filtering logic for pickups and deliveries adapts to client capability flags. Type system, UI components, and layouts are updated throughout to support these changes.

Changes

Forms, Inventory, and Filtering Enhancements

Layer / File(s) Summary
Type System and Data Contracts
types/general.ts, types/client-requests.ts, types/inventory.ts, app/donate/DonorFormContext.tsx
Core types establish required apt field in addresses, optional email in clients, preferredLanguage in client questions, and removal of size from inventory/donation items.
Client Request Form Validation and Fields
app/client-request-form/steps/Step1ClientInfo.tsx, app/client-request-form/steps/Step2Requests.tsx, app/client-request-form/steps/Step3Review.tsx
Step1 tightens validation to require explicit family/language selections, enforce whole-number counts, validate 5-digit ZIP codes, and require apartment field; email becomes conditionally validated; preferred language input renders conditionally. Step2 softens availability wording. Step3 conditionally displays preferred language.
Photo Upload Component and Styling Updates
components/form/PhotoDropzone.tsx, components/form/FormInput.tsx, app/donate/layout.tsx, app/globals.css, app/donate/steps/Step1PersonalInfo.tsx, app/donate/steps/Step2AddDonations.tsx, app/donate/steps/Step3Review.tsx, app/donate/steps/Step4Confirmation.tsx
New PhotoDropzone component supports drag-and-drop and click uploads with 4-photo max and error handling. FormInput accepts step and maxLength. Logo sizing and responsive layouts updated across donation steps. Global font shorthand replaced by explicit font-family.
Donation Form Validation and PhotoDropzone Integration
app/donate/steps/Step1PersonalInfo.tsx, app/donate/steps/Step2AddDonations.tsx, app/donate/steps/Step3Review.tsx, app/donate/steps/Step4Confirmation.tsx
Step1PersonalInfo adds derived city/hearing state with custom "Other" input, validates address.city and 5-digit ZIP, scrolls to errors. Step2AddDonations validates per-item (word count, required category, quantity, photos), integrates PhotoDropzone with inline errors and trash deletion. Step3Review removes size handling, initializes apt, uses responsive grids. Step4 updates timing language.
Inventory Category Deletion with Confirmation
components/control-panel/CategoryModal.tsx, app/control-panel/categories/page.tsx, lib/queries/inventory.ts, lib/services/inventory.ts, package.json
New delete service and React Query mutation with optimistic updates. Modal accepts onDelete prop and displays confirmation. CategoriesPage wires delete and refactors layout for sticky headers. Category ID generation switched to uuidv4(). react-dropzone dependency added.
Request Filtering Based on Client Capabilities
app/pickups-deliveries/scheduled/page.tsx, app/pickups-deliveries/unscheduled/page.tsx, components/pickups-deliveries/Request.tsx, components/schedule/ShiftDetailOverlay.tsx
Scheduled/unscheduled tasks filter donation and client requests based on canDropOff and canPickUp flags. Request component sums item quantities and conditionally shows email. ShiftDetailOverlay requires apt parameter.
Display and Layout Updates for Removed Size Field
components/donation-requests/DRContentsTable.tsx, components/donation-requests/ItemReviewModal.tsx, components/inventory/WarehouseGallery.tsx, components/client-requests/RequestDetails.tsx, components/form/StepIndicator.tsx
DRContentsTable adjusts Category column to 20% and removes Size. ItemReviewModal replaces size badge with photo carousel supporting multiple photos. WarehouseGallery replaces size badge with category and quantity. RequestDetails conditionally renders preferred language and uses raw boolean/nullish handling. StepIndicator updates responsive visibility and text styling.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

🚥 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 'Joel/forms revamp' is vague and generic, using non-descriptive terms that do not clearly convey what was changed in the changeset. Provide a more specific title that describes the main changes, such as 'Refactor client and donation forms with enhanced validation and UI updates' or 'Update form validation, remove size field, and improve address handling'.
✅ 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.


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.

Copy link
Copy Markdown

@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: 9

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
app/donate/steps/Step3Review.tsx (2)

50-50: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fix forbidden UUID generation and release photo object URLs in Step3Review

  • Replace crypto.randomUUID() (at app/donate/steps/Step3Review.tsx:50 and :70) with uuidv4 per the project UUID rule (import v4 as uuidv4 from "uuid" and update both IDs).
  • URL.createObjectURL(photo) (around :250) is used for previews without URL.revokeObjectURL(...) cleanup; add revocation (e.g., via useEffect) to avoid leaking object URLs.
🤖 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 `@app/donate/steps/Step3Review.tsx` at line 50, Replace forbidden
crypto.randomUUID() uses in Step3Review with the project's UUID helper: import {
v4 as uuidv4 } from "uuid" and replace the two id assignments (the ones
currently using crypto.randomUUID() at the spots around id: crypto.randomUUID(),
and the second occurrence) with uuidv4(). Also stop leaking image object URLs
created with URL.createObjectURL(photo) by creating a previewUrl state (or using
the existing preview variable) and adding a useEffect that calls
URL.revokeObjectURL(previewUrl) when the photo/previewUrl changes or when the
component unmounts; ensure revoke runs in the cleanup function so every created
object URL is revoked.

250-250: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Avoid URL.createObjectURL in render without cleanup

URL.createObjectURL(photo) is created inline in render (e.g., app/donate/steps/Step3Review.tsx and components/form/PhotoDropzone.tsx) and there’s no URL.revokeObjectURL cleanup anywhere in the repo, which can leak memory across rerenders/navigation.

🤖 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 `@app/donate/steps/Step3Review.tsx` at line 250, The image src currently uses
URL.createObjectURL(photo) inline in render (in the Step3Review component and
PhotoDropzone component), which leaks object URLs; change to create the object
URL in an effect and store it in state (e.g., photoPreview or previewUrl) and
use that state for src, and inside the same useEffect return a cleanup that
calls URL.revokeObjectURL(previewUrl); update any places referencing
src={URL.createObjectURL(photo)} to use the new preview state and ensure cleanup
runs whenever the photo/file changes or the component unmounts.
🤖 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 `@app/client-request-form/steps/Step1ClientInfo.tsx`:
- Around line 49-50: The validation currently allows whitespace-only values for
preferredLanguage and apt; update the checks in Step1ClientInfo (where
client.questions.clientSpeaksEnglish / client.questions.preferredLanguage and
the apt validation at the other check) to call .trim() on the string before
testing emptiness (e.g., use client.questions.preferredLanguage?.trim() and
apt?.trim()) and only set newErrors.preferredLanguage or newErrors.apt when the
trimmed value is empty; preserve existing branching logic for
clientSpeaksEnglish and required fields.

In `@app/donate/steps/Step1PersonalInfo.tsx`:
- Around line 64-66: Step1PersonalInfo: the apartment/unit field (address.apt)
is now required but not validated or marked in the UI; update the validation in
the component (where formState.donorInfo.address is checked and newErrors is
populated) to check formState.donorInfo.address?.apt and set newErrors.apt =
"Apt/Unit is required" when empty, and also mark the Apt input element as
required (add the required attribute/prop and any aria-required) so users cannot
proceed without it; apply the same validation and UI required flag in the other
related validation block referenced around the 216-232 region.
- Line 320: The string in Step1PersonalInfo (inside the JSX text block near the
pickup-range message) contains an unescaped apostrophe ("isn't"), triggering
react/no-unescaped-entities; update that text to escape the apostrophe (e.g.,
replace "isn't" with "isn't" or "isn&`#39`;t") in the JSX within the
Step1PersonalInfo component so the lint rule is satisfied.
- Around line 97-100: The scroll-to-error logic in the setTimeout callback
inside Step1PersonalInfo.tsx only queries ".form-error" and therefore misses
acknowledgement errors; update the callback used with setTimeout to query for
multiple selectors (e.g., ".form-error, .acknowledgement-error,
[aria-invalid='true']") or otherwise include the acknowledgement error element
(or its aria-invalid state) so the first matching element is found and scrolled
into view using scrollIntoView; modify the document.querySelector call in that
setTimeout block accordingly to pick the appropriate selector(s).

In `@app/donate/steps/Step2AddDonations.tsx`:
- Around line 224-229: The delete button in Step2AddDonations.tsx is an
icon-only control and needs an accessible name; update the button element that
calls removeDonationItem(item.id) to include an aria-label (for example "Remove
item" or "Remove item {item.name}" / include item.id for uniqueness) so screen
readers can identify the control; ensure the attribute is added to the same
button element that renders the Trash icon.

In `@components/client-requests/RequestDetails.tsx`:
- Line 72: Replace the direct nullish coalescing that renders raw booleans for
questions.isVeteran with a ternary that maps true->"Yes" and false->"No", while
still falling back to "—" when the value is null/undefined; locate the display
in RequestDetails.tsx where questions.isVeteran is used and change the rendering
logic to first check for null/undefined and otherwise return "Yes" or "No".
- Line 76: The boolean field questions.wasChronic is being rendered raw which
shows "true"/"false"; update the rendering in RequestDetails (where the JSX
contains questions.wasChronic) to map true->"Yes", false->"No" and keep the
fallback "—" for null/undefined (e.g., use a conditional/ternary that checks
questions.wasChronic === true ? "Yes" : questions.wasChronic === false ? "No" :
"—"). Ensure the same pattern is applied consistently for other boolean fields
in this component that previously rendered raw booleans.

In `@components/form/PhotoDropzone.tsx`:
- Around line 79-88: The remove-photo button (the icon-only element rendering
CloseIcon with the onClick that calls onChange(photos.filter((p) => p !==
photo))) is missing an accessible name; add an aria-label (e.g.,
aria-label="Remove photo" or aria-label={`Remove photo: ${photo.name ||
index}`}) to the button element so screen readers can describe the action while
keeping the existing onClick, onChange, photos, photo and CloseIcon logic
intact.
- Around line 75-76: The render currently calls URL.createObjectURL(photo)
directly in PhotoDropzone which leaks object URLs; change to create and store
the object URL in a React state (e.g., photoPreviewUrl) and use a useEffect that
sets photoPreviewUrl = URL.createObjectURL(photo) when photo changes and calls
URL.revokeObjectURL(oldUrl) in the cleanup (and on unmount) to avoid leaks. Also
update the icon-only remove <button> that renders CloseIcon to include an
accessible name (add an aria-label and/or title like "Remove photo") so screen
readers can announce the action; ensure the same button handler (e.g.,
onRemovePhoto or handleRemove) is preserved.

---

Outside diff comments:
In `@app/donate/steps/Step3Review.tsx`:
- Line 50: Replace forbidden crypto.randomUUID() uses in Step3Review with the
project's UUID helper: import { v4 as uuidv4 } from "uuid" and replace the two
id assignments (the ones currently using crypto.randomUUID() at the spots around
id: crypto.randomUUID(), and the second occurrence) with uuidv4(). Also stop
leaking image object URLs created with URL.createObjectURL(photo) by creating a
previewUrl state (or using the existing preview variable) and adding a useEffect
that calls URL.revokeObjectURL(previewUrl) when the photo/previewUrl changes or
when the component unmounts; ensure revoke runs in the cleanup function so every
created object URL is revoked.
- Line 250: The image src currently uses URL.createObjectURL(photo) inline in
render (in the Step3Review component and PhotoDropzone component), which leaks
object URLs; change to create the object URL in an effect and store it in state
(e.g., photoPreview or previewUrl) and use that state for src, and inside the
same useEffect return a cleanup that calls URL.revokeObjectURL(previewUrl);
update any places referencing src={URL.createObjectURL(photo)} to use the new
preview state and ensure cleanup runs whenever the photo/file changes or the
component unmounts.
🪄 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: 3622c4d0-222d-4f4e-910c-1623829a8ea3

📥 Commits

Reviewing files that changed from the base of the PR and between f83920c and 2c45625.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (28)
  • app/client-request-form/steps/Step1ClientInfo.tsx
  • app/client-request-form/steps/Step2Requests.tsx
  • app/client-request-form/steps/Step3Review.tsx
  • app/control-panel/categories/page.tsx
  • app/donate/DonorFormContext.tsx
  • app/donate/layout.tsx
  • app/donate/steps/Step1PersonalInfo.tsx
  • app/donate/steps/Step2AddDonations.tsx
  • app/donate/steps/Step3Review.tsx
  • app/donate/steps/Step4Confirmation.tsx
  • app/globals.css
  • app/pickups-deliveries/scheduled/page.tsx
  • app/pickups-deliveries/unscheduled/page.tsx
  • components/client-requests/RequestDetails.tsx
  • components/control-panel/CategoryModal.tsx
  • components/donation-requests/DRContentsTable.tsx
  • components/donation-requests/ItemReviewModal.tsx
  • components/form/PhotoDropzone.tsx
  • components/form/StepIndicator.tsx
  • components/inventory/WarehouseGallery.tsx
  • components/pickups-deliveries/Request.tsx
  • components/schedule/ShiftDetailOverlay.tsx
  • lib/queries/inventory.ts
  • lib/services/inventory.ts
  • package.json
  • types/client-requests.ts
  • types/general.ts
  • types/inventory.ts
💤 Files with no reviewable changes (3)
  • app/donate/DonorFormContext.tsx
  • components/inventory/WarehouseGallery.tsx
  • types/inventory.ts

Comment thread app/client-request-form/steps/Step1ClientInfo.tsx Outdated
Comment on lines +64 to +66
if (!formState.donorInfo.address?.city) {
newErrors.city = "City/Town is required";
}
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 | 🟠 Major | ⚡ Quick win

Enforce required apartment/unit in validation and UI.

apt is now part of required address input, but this step doesn’t validate it and the field isn’t marked required, so users can proceed with an empty apartment/unit.

Suggested fix
     if (!formState.donorInfo.address?.streetAddress) {
       newErrors.streetAddress = "Street address is required";
     }
+    if (!formState.donorInfo.address?.apt?.trim()) {
+      newErrors.apt = "Apt / Unit is required";
+    }
     if (!formState.donorInfo.address?.city) {
       newErrors.city = "City/Town is required";
     }
@@
           <FormInput
             label="Apt / Unit"
+            required
             value={formState.donorInfo.address?.apt || ""}
@@
           />
+          {errors.apt && <p className="form-error text-red-500 text-sm mt-1">{errors.apt}</p>}

Also applies to: 216-232

🤖 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 `@app/donate/steps/Step1PersonalInfo.tsx` around lines 64 - 66,
Step1PersonalInfo: the apartment/unit field (address.apt) is now required but
not validated or marked in the UI; update the validation in the component (where
formState.donorInfo.address is checked and newErrors is populated) to check
formState.donorInfo.address?.apt and set newErrors.apt = "Apt/Unit is required"
when empty, and also mark the Apt input element as required (add the required
attribute/prop and any aria-required) so users cannot proceed without it; apply
the same validation and UI required flag in the other related validation block
referenced around the 216-232 region.

Comment thread app/donate/steps/Step1PersonalInfo.tsx
Comment thread app/donate/steps/Step1PersonalInfo.tsx Outdated
Comment on lines 224 to 229
<button
type="button"
onClick={() => removeDonationItem(item.id)}
className="text-red-500 hover:text-red-700"
>
<svg
className="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
//svg path for the icon
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
/>
</svg>
<Trash className="w-4 h-4 text-text-1" />
</button>
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

Add aria-label to the item delete button.

This icon-only control should expose an accessible name (e.g., aria-label="Remove item").

🤖 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 `@app/donate/steps/Step2AddDonations.tsx` around lines 224 - 229, The delete
button in Step2AddDonations.tsx is an icon-only control and needs an accessible
name; update the button element that calls removeDonationItem(item.id) to
include an aria-label (for example "Remove item" or "Remove item {item.name}" /
include item.id for uniqueness) so screen readers can identify the control;
ensure the attribute is added to the same button element that renders the Trash
icon.

<div className="pl-4 min-h-10 flex items-start py-2 border-t border-light-border">{questions.childrenInFamily ?? "—"}</div>
<div className="font-bold text-sm text-black pl-4 min-h-10 bg-[#FAFAFB] flex items-start py-2 border-t border-r border-light-border">Veteran status</div>
<div className="pl-4 min-h-10 flex items-start py-2 border-t border-light-border">{questions.isVeteran == null ? "—" : questions.isVeteran ? "Yes" : "No"}</div>
<div className="pl-4 min-h-10 flex items-start py-2 border-t border-light-border">{questions.isVeteran ?? ""}</div>
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 | 🔴 Critical | ⚡ Quick win

Boolean values will display as "true"/"false" instead of "Yes"/"No".

The nullish coalescing operator ?? will render the raw boolean value when questions.isVeteran is true or false, displaying "true" or "false" to users instead of the expected "Yes" or "No".

🐛 Proposed fix
-                    <div className="pl-4 min-h-10 flex items-start py-2 border-t border-light-border">{questions.isVeteran ?? "—"}</div>
+                    <div className="pl-4 min-h-10 flex items-start py-2 border-t border-light-border">
+                        {questions.isVeteran == null ? "—" : (questions.isVeteran ? "Yes" : "No")}
+                    </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.

Suggested change
<div className="pl-4 min-h-10 flex items-start py-2 border-t border-light-border">{questions.isVeteran ?? "—"}</div>
<div className="pl-4 min-h-10 flex items-start py-2 border-t border-light-border">
{questions.isVeteran == null ? "—" : (questions.isVeteran ? "Yes" : "No")}
</div>
🤖 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 `@components/client-requests/RequestDetails.tsx` at line 72, Replace the direct
nullish coalescing that renders raw booleans for questions.isVeteran with a
ternary that maps true->"Yes" and false->"No", while still falling back to "—"
when the value is null/undefined; locate the display in RequestDetails.tsx where
questions.isVeteran is used and change the rendering logic to first check for
null/undefined and otherwise return "Yes" or "No".

<div className="pl-4 min-h-10 flex items-start py-2 border-t border-light-border">{questions.canPickUp ? "Yes" : "No"}</div>
<div className="font-bold text-sm text-black pl-4 min-h-10 bg-[#FAFAFB] flex items-start py-2 border-t border-r border-light-border">Chronic before housing?</div>
<div className="pl-4 min-h-10 flex items-start py-2 border-t border-light-border">{questions.wasChronic == null ? "—" : questions.wasChronic ? "Yes" : "No"}</div>
<div className="pl-4 min-h-10 flex items-start py-2 border-t border-light-border">{questions.wasChronic ?? ""}</div>
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 | 🔴 Critical | ⚡ Quick win

Boolean values will display as "true"/"false" instead of "Yes"/"No".

Same issue as veteran status above: the raw boolean will be rendered to users instead of "Yes" or "No".

🐛 Proposed fix
-                    <div className="pl-4 min-h-10 flex items-start py-2 border-t border-light-border">{questions.wasChronic ?? "—"}</div>
+                    <div className="pl-4 min-h-10 flex items-start py-2 border-t border-light-border">
+                        {questions.wasChronic == null ? "—" : (questions.wasChronic ? "Yes" : "No")}
+                    </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.

Suggested change
<div className="pl-4 min-h-10 flex items-start py-2 border-t border-light-border">{questions.wasChronic ?? "—"}</div>
<div className="pl-4 min-h-10 flex items-start py-2 border-t border-light-border">
{questions.wasChronic == null ? "—" : (questions.wasChronic ? "Yes" : "No")}
</div>
🤖 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 `@components/client-requests/RequestDetails.tsx` at line 76, The boolean field
questions.wasChronic is being rendered raw which shows "true"/"false"; update
the rendering in RequestDetails (where the JSX contains questions.wasChronic) to
map true->"Yes", false->"No" and keep the fallback "—" for null/undefined (e.g.,
use a conditional/ternary that checks questions.wasChronic === true ? "Yes" :
questions.wasChronic === false ? "No" : "—"). Ensure the same pattern is applied
consistently for other boolean fields in this component that previously rendered
raw booleans.

Comment thread components/form/PhotoDropzone.tsx Outdated
Comment on lines +75 to +76
src={URL.createObjectURL(photo)}
alt="Item Photo"
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 | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate and display the relevant section of the file with line numbers
FILE="components/form/PhotoDropzone.tsx"
if [ ! -f "$FILE" ]; then
  echo "Missing file: $FILE"
  exit 1
fi

# Show around the referenced line range (adjust if file shorter)
nl -ba "$FILE" | sed -n '1,220p' | sed -n '60,130p'

# Also scan for object URL usage and any revoke calls
rg -n "createObjectURL|revokeObjectURL" "$FILE" || true
rg -n "remove|trash|icon|aria-label|aria-labelledby|title" "$FILE" || true

Repository: Hack4Impact-UMD/journey-home

Length of output: 116


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="components/form/PhotoDropzone.tsx"
test -f "$FILE" || { echo "Missing file: $FILE"; exit 1; }

echo "== createObjectURL / revokeObjectURL in file =="
rg -n "createObjectURL|revokeObjectURL" "$FILE" || true

echo
echo "== Remove/trash/button accessibility hints in file =="
rg -n "(remove|trash|icon|aria-label|aria-labelledby|title=|role=)" "$FILE" || true

echo
echo "== Lines 60-130 of $FILE (with line numbers via awk) =="
awk 'NR>=60 && NR<=130 {printf "%5d  %s\n", NR, $0}' "$FILE"

Repository: Hack4Impact-UMD/journey-home

Length of output: 2799


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find CloseIcon component file
fd -a "CloseIcon" . 2>/dev/null | head -n 50

# Also try common paths
fd -a "CloseIcon.tsx" . 2>/dev/null | head -n 50

# Print the CloseIcon.tsx content if found
FILES=$(fd -a "CloseIcon\\.tsx" . || true)
if [ -z "$FILES" ]; then
  echo "No CloseIcon.tsx found"
  exit 0
fi

echo "== CloseIcon candidates =="
echo "$FILES"

# show the first candidate file with some lines
FIRST=$(echo "$FILES" | head -n1)
echo
echo "== Contents of $FIRST =="
awk 'NR>=1 && NR<=200 {print}' "$FIRST"

Repository: Hack4Impact-UMD/journey-home

Length of output: 2527


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find CloseIcon component file
fd -a "CloseIcon" . 2>/dev/null | head -n 50
echo
fd -a "CloseIcon.tsx" . 2>/dev/null | head -n 50

FILES=$(fd -a "CloseIcon\\.tsx" . || true)
if [ -z "$FILES" ]; then
  echo "No CloseIcon.tsx found"
  exit 0
fi

echo
echo "== CloseIcon candidates =="
echo "$FILES"

FIRST=$(echo "$FILES" | head -n1)
echo
echo "== Contents of $FIRST =="
awk 'NR>=1 && NR<=200 {print}' "$FIRST"

Repository: Hack4Impact-UMD/journey-home

Length of output: 2529


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="components/form/PhotoDropzone.tsx"
awk 'NR>=1 && NR<=90 {printf "%5d  %s\n", NR, $0}' "$FILE"

Repository: Hack4Impact-UMD/journey-home

Length of output: 3998


Fix object URL lifecycle and add an accessible name for the photo remove button

  • components/form/PhotoDropzone.tsx (line 75) calls URL.createObjectURL(photo) during render, but never calls URL.revokeObjectURL, which can leak memory as photos are added/removed.
  • The icon-only remove <button> using CloseIcon (lines 79-88) has no accessible name (aria-label/title), so the action isn’t announced to screen readers.
Suggested fix (object URL revocation)
-import { useCallback } from "react";
+import { useCallback, useEffect, useMemo } from "react";

 export default function PhotoDropzone({ photos, onChange, error }: PhotoDropzoneProps) {
+  const previewUrls = useMemo(
+    () => photos.map((photo) => ({ photo, url: URL.createObjectURL(photo) })),
+    [photos]
+  );
+
+  useEffect(() => {
+    return () => {
+      previewUrls.forEach(({ url }) => URL.revokeObjectURL(url));
+    };
+  }, [previewUrls]);
+
   const onDrop = useCallback(
@@
-              {photos.map((photo, index) => (
+              {previewUrls.map(({ photo, url }, index) => (
                 <div className="relative" key={index + " " + photo.name}>
                   <img
-                    src={URL.createObjectURL(photo)}
+                    src={url}
                     alt="Item Photo"
                     className="max-h-20 max-w-20 md:max-h-32 md:max-w-32 rounded-sm object-contain"
                   />
📝 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
src={URL.createObjectURL(photo)}
alt="Item Photo"
import { useCallback, useEffect, useMemo } from "react";
export default function PhotoDropzone({ photos, onChange, error }: PhotoDropzoneProps) {
const previewUrls = useMemo(
() => photos.map((photo) => ({ photo, url: URL.createObjectURL(photo) })),
[photos]
);
useEffect(() => {
return () => {
previewUrls.forEach(({ url }) => URL.revokeObjectURL(url));
};
}, [previewUrls]);
const onDrop = useCallback(
// ... rest of onDrop implementation
);
return (
// ... JSX structure
{previewUrls.map(({ photo, url }, index) => (
<div className="relative" key={index + " " + photo.name}>
<img
src={url}
alt="Item Photo"
className="max-h-20 max-w-20 md:max-h-32 md:max-w-32 rounded-sm object-contain"
/>
🤖 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 `@components/form/PhotoDropzone.tsx` around lines 75 - 76, The render currently
calls URL.createObjectURL(photo) directly in PhotoDropzone which leaks object
URLs; change to create and store the object URL in a React state (e.g.,
photoPreviewUrl) and use a useEffect that sets photoPreviewUrl =
URL.createObjectURL(photo) when photo changes and calls
URL.revokeObjectURL(oldUrl) in the cleanup (and on unmount) to avoid leaks. Also
update the icon-only remove <button> that renders CloseIcon to include an
accessible name (add an aria-label and/or title like "Remove photo") so screen
readers can announce the action; ensure the same button handler (e.g.,
onRemovePhoto or handleRemove) is preserved.

Comment on lines +79 to +88
<button
type="button"
onClick={(e) => {
e.stopPropagation();
onChange(photos.filter((p) => p !== photo));
}}
className="absolute top-[-.5em] right-[-.5em] bg-white rounded-full text-white text-lg"
>
<CloseIcon />
</button>
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

Add an accessible name to the remove-photo button.

The icon-only button needs an aria-label so screen-reader users can remove a photo.

🤖 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 `@components/form/PhotoDropzone.tsx` around lines 79 - 88, The remove-photo
button (the icon-only element rendering CloseIcon with the onClick that calls
onChange(photos.filter((p) => p !== photo))) is missing an accessible name; add
an aria-label (e.g., aria-label="Remove photo" or aria-label={`Remove photo:
${photo.name || index}`}) to the button element so screen readers can describe
the action while keeping the existing onClick, onChange, photos, photo and
CloseIcon logic intact.

Copy link
Copy Markdown

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
components/client-requests/RequestDetails.tsx (1)

61-65: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Gate preferred-language rendering on explicit false.

!questions.clientSpeaksEnglish also matches undefined/null, so the “Preferred language” row shows before that question is answered.

Suggested fix
-                    {!questions.clientSpeaksEnglish && (
+                    {questions.clientSpeaksEnglish === false && (
                         <>
                             <div className="font-bold text-sm text-black pl-4 min-h-10 bg-[`#FAFAFB`] flex items-start py-2 border-t border-r border-light-border">Preferred language</div>
                             <div className="pl-4 min-h-10 flex items-start py-2 border-t border-light-border">{questions.preferredLanguage || "—"}</div>
                         </>
                     )}
🤖 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 `@components/client-requests/RequestDetails.tsx` around lines 61 - 65, The
Preferred language row is rendering when questions.clientSpeaksEnglish is
undefined/null because the code uses !questions.clientSpeaksEnglish; change the
conditional to explicitly check for false (questions.clientSpeaksEnglish ===
false) so the row only shows when the client explicitly does not speak English,
keeping the display of questions.preferredLanguage (or fallback "—") unchanged;
update the JSX around the Preferred language block that references
questions.clientSpeaksEnglish and questions.preferredLanguage accordingly.
♻️ Duplicate comments (2)
app/donate/steps/Step1PersonalInfo.tsx (1)

61-66: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Apt / Unit is still optional in validation and UI.

This step can proceed with an empty apartment/unit because validateForm doesn’t enforce address.apt, and the Apt field is not marked required or shown with an inline error.

Suggested fix
   if (!formState.donorInfo.address?.streetAddress) {
     newErrors.streetAddress = "Street address is required";
   }
+  if (!formState.donorInfo.address?.apt?.trim()) {
+    newErrors.apt = "Apt / Unit is required";
+  }
   if (!formState.donorInfo.address?.city) {
     newErrors.city = "City/Town is required";
   }

   <FormInput
     label="Apt / Unit"
+    required
     value={formState.donorInfo.address?.apt || ""}
     onChange={(e) =>
       updateDonorInfo({
         address: {
@@
       })
     }
   />
+  {errors.apt && <p className="form-error text-red-500 text-sm mt-1">{errors.apt}</p>}

Also applies to: 216-232

🤖 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 `@app/donate/steps/Step1PersonalInfo.tsx` around lines 61 - 66, The Apt/Unit
field is not being validated or shown as required; update the validation in
Step1PersonalInfo (the validateForm logic that checks
formState.donorInfo.address.*) to enforce formState.donorInfo.address.apt and
set newErrors.apt when empty, and update the Apt input JSX in the
Step1PersonalInfo component to include required/aria-required and render an
inline error message (reuse the same error display pattern used for
streetAddress/city); also make the identical change to the other validation
block in this file that mirrors the same address checks so both places validate
address.apt consistently.
components/form/PhotoDropzone.tsx (1)

110-119: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add an accessible name to the remove-photo button.

The icon-only control still has no aria-label, so assistive tech can’t announce the action.

Suggested fix
                   <button
                     type="button"
+                    aria-label={`Remove photo ${index + 1}`}
+                    title="Remove photo"
                     onClick={(e) => {
                       e.stopPropagation();
                       onChange(photos.filter((p) => p !== photo));
                     }}
                     className="absolute top-[-.5em] right-[-.5em] bg-white rounded-full text-white text-lg"
                   >
🤖 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 `@components/form/PhotoDropzone.tsx` around lines 110 - 119, The remove-photo
button in PhotoDropzone is an icon-only control (uses CloseIcon) and needs an
accessible name; update the button element rendered in PhotoDropzone to include
an aria-label (e.g., aria-label="Remove photo") or aria-labelledby that
references a visible label so assistive tech can announce the action; ensure the
onClick handler (which calls onChange(photos.filter((p) => p !== photo)))
remains unchanged and that the aria attribute is applied to that same button
element.
🤖 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.

Outside diff comments:
In `@components/client-requests/RequestDetails.tsx`:
- Around line 61-65: The Preferred language row is rendering when
questions.clientSpeaksEnglish is undefined/null because the code uses
!questions.clientSpeaksEnglish; change the conditional to explicitly check for
false (questions.clientSpeaksEnglish === false) so the row only shows when the
client explicitly does not speak English, keeping the display of
questions.preferredLanguage (or fallback "—") unchanged; update the JSX around
the Preferred language block that references questions.clientSpeaksEnglish and
questions.preferredLanguage accordingly.

---

Duplicate comments:
In `@app/donate/steps/Step1PersonalInfo.tsx`:
- Around line 61-66: The Apt/Unit field is not being validated or shown as
required; update the validation in Step1PersonalInfo (the validateForm logic
that checks formState.donorInfo.address.*) to enforce
formState.donorInfo.address.apt and set newErrors.apt when empty, and update the
Apt input JSX in the Step1PersonalInfo component to include
required/aria-required and render an inline error message (reuse the same error
display pattern used for streetAddress/city); also make the identical change to
the other validation block in this file that mirrors the same address checks so
both places validate address.apt consistently.

In `@components/form/PhotoDropzone.tsx`:
- Around line 110-119: The remove-photo button in PhotoDropzone is an icon-only
control (uses CloseIcon) and needs an accessible name; update the button element
rendered in PhotoDropzone to include an aria-label (e.g., aria-label="Remove
photo") or aria-labelledby that references a visible label so assistive tech can
announce the action; ensure the onClick handler (which calls
onChange(photos.filter((p) => p !== photo))) remains unchanged and that the aria
attribute is applied to that same button element.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 7be9ac47-8dfd-4aec-b607-96b6e107f3ec

📥 Commits

Reviewing files that changed from the base of the PR and between 2c45625 and 3b6a1f8.

📒 Files selected for processing (9)
  • app/client-request-form/steps/Step1ClientInfo.tsx
  • app/client-requests/admin/new/page.tsx
  • app/client-requests/admin/reviewed/page.tsx
  • app/donate/steps/Step1PersonalInfo.tsx
  • app/donate/steps/Step3Review.tsx
  • components/client-requests/RequestDetails.tsx
  • components/form/FormInput.tsx
  • components/form/PhotoDropzone.tsx
  • lib/services/inventory.ts
✅ Files skipped from review due to trivial changes (1)
  • app/client-requests/admin/new/page.tsx

@joelchem joelchem merged commit 0a61b80 into main May 25, 2026
3 checks passed
@joelchem joelchem deleted the joel/forms-revamp branch May 25, 2026 19: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