Skip to content

Feat/web product listing 1 2#300

Merged
onerandomdevv merged 7 commits into
devfrom
feat/web-product-listing-1-2
May 21, 2026
Merged

Feat/web product listing 1 2#300
onerandomdevv merged 7 commits into
devfrom
feat/web-product-listing-1-2

Conversation

@amoomustakim-hue
Copy link
Copy Markdown
Collaborator

@amoomustakim-hue amoomustakim-hue commented May 21, 2026

What does this PR do?

Builds W-15a: Product Listing Form Screens 1 + 2.

This PR adds the first half of the store product listing wizard:

  • Screen 1: Photos
  • Screen 2: Product Details

It includes product image upload integration with the backend upload/moderation endpoint, per-photo moderation feedback, thumbnail ordering, retry/remove actions, product detail fields, category selection, store tags, product specs, SKU, and bundle footwear toggle.

This PR does not publish products and does not implement Screens 3 or 4. W-15b will continue the wizard with variants, pricing, size guide, and publish flow.

Type of change

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

How to test this

  1. Checkout feat/web-product-listing-1-2

  2. Run:

    cd apps/web
    pnpm run lint
    npx tsc --noEmit
    pnpm run build

  3. Visit:

    /store/products/new

  4. Confirm Screen 1:

    • Image upload UI renders on mobile and desktop
    • Up to 5 images can be uploaded
    • Upload uses multipart/form-data
    • Per-photo moderation state is shown
    • SAFE shows as Approved
    • SENSITIVE shows as Sensitive
    • BLOCKED/rejected images do not count
    • User cannot continue without at least one usable uploaded image
    • Move left/right controls work
    • Retry/remove actions work
  5. Confirm Screen 2:

    • Product name counter works, max 100 chars
    • Description counter works, max 1000 chars
    • Category dropdown uses documented MVP categories
    • Sub-category only appears for relevant categories
    • Store tags max 3 enforced
    • Specs max 20 rows enforced
    • SKU field exists
    • “This product includes shoes” toggle exists
  6. Confirm:

    • No POST /products call is made
    • Screens 3 and 4 are not implemented
    • No backend/env files changed

Pre-commit checklist

  • Web lint/type/build commands pass
  • No backend files changed
  • No env files changed
  • No unrelated task cleanup included
  • No Screen 3/4 implementation included
  • No product publish call included
  • No hardcoded hex values in page/component files
  • No emojis added to UI
  • Touch targets meet 44px minimum

Notes

The upload flow uses raw fetch with FormData because the existing API client sets Content-Type: application/json, which would break multipart uploads.

SENSITIVE images are not labelled as approved. They are displayed as Sensitive and count as usable only because the backend successfully accepted/uploaded them with a moderation flag.

Summary by CodeRabbit

Release Notes

  • New Features
    • Launched multi-step product creation wizard for adding new products to your store
    • Step 1: Upload up to 5 product photos with drag-and-drop support, mobile camera/gallery access, reorder and remove photos, and view moderation status
    • Step 2: Enter product details including name, description, category, subcategory, tags, and specifications
    • Visual progress tracker displays your current step in the wizard

Review Change Stack

amoomustakim-hue and others added 4 commits May 21, 2026 14:49
Implements Screens 1 and 2 of the 4-step product listing wizard.
Screens 3 and 4 (Variants & Pricing, Size Guide & Publish) continue in W-15b.
POST /products is NOT called in this task — no product is created.

Files created:
- src/lib/product-listing.ts
    Types (UploadedPhoto, ProductListingDraft, SpecRow)
    uploadProductImage() — raw fetch FormData to POST /upload/image
    Platform category constants (13 of 20 documented)
    GuideType sub-category constants (from Prisma enum)
- store/products/new/page.tsx — 'use client' wizard, 4-step progress
- _components/StepProgress.tsx — step indicator (steps 1-4)
- _components/ScreenOnePhotos.tsx
    Drag-and-drop upload zone (desktop), Take Photo / Choose Photo buttons (mobile)
    Concurrent multipart upload via POST /upload/image
    Per-photo status: uploading / approved / sensitive / rejected / error
    Move left/right reorder, retry on failure, remove
    Gate: cannot advance without ≥1 approved (SAFE or SENSITIVE) photo
- _components/ScreenTwoDetails.tsx
    Product name (max 100, live counter)
    Description textarea (max 1000, live counter)
    Platform category select (13 documented categories)
    Sub-category select (FASHION / BABY_AND_KIDS trigger, GuideType enum)
    Tag chip input (max 3, max 30 chars each, Enter/comma to add)
    Specs accordion (max 20 rows, attribute 50 / value 200 chars)
    SKU optional field ("Your internal product code")
    Bundle toggle ("This product includes shoes") — local state only;
      no matching field in CreateProductDto yet (documented limitation)

Upload notes:
- Uses raw fetch NOT api.ts — api.ts injects Content-Type: application/json
  which breaks multipart/form-data. Browser sets boundary automatically.
- SENSITIVE images count as approved (upload succeeded, just flagged).
- BLOCKED images show error + reason + retry/remove controls.

Category notes:
- 13 of 20 documented categories included; full 20 needs seed file inspection.
- platformCategory is a String in the DTO (no enum constraint).

No backend files changed. No env files changed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Categories:
- Replace 13 placeholder values with the 19 documented MVP platform
  categories (Fashion, Shoes & Footwear, Bags & Accessories, etc.)
- Update CATEGORIES_WITH_SUBCATEGORY to match new string values
  ("Fashion", "Shoes & Footwear", "Baby & Kids")

Moderation labels:
- SAFE   → "Approved" (unchanged)
- SENSITIVE → "Sensitive" badge (unchanged visually, but no longer
  described as "approved" in copy or helper names)
- BLOCKED / error → "Blocked" / "Failed" (unchanged)
- Rename countApprovedPhotos → usableImageCount; SENSITIVE images
  still pass the Screen 1 gate (upload succeeded) but are not called
  "approved" in the UI
- Gate message: "Add at least 1 photo" (not "approved photo")
- Count label: "X of 5 photos uploaded" (not "approved")
- Description: "first uploaded photo" (not "approved photo")

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Guard against missing NEXT_PUBLIC_API_URL: throw a typed UploadApiError
  immediately rather than silently sending a fetch to an empty URL
- Wrap res.json() in try/catch: non-JSON responses (HTML error pages from
  reverse proxies, CDN errors) now throw a clear INVALID_RESPONSE error
  instead of crashing the upload flow with an unhandled parse exception

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ScreenOnePhotos.tsx:
- Convert drag-drop zone from <div role="button"> to <button type="button">
  so that Space natively triggers click (and scroll prevention is handled
  by the browser) without needing an explicit onKeyDown + preventDefault
- Remove now-redundant onKeyDown, tabIndex={0}, and role="button" attributes
- Add w-full so the button fills its container correctly

page.tsx:
- Fix memory leak in the object URL cleanup effect: the previous version
  closed over draft.photos at mount time (empty array), so revokeObjectURL
  never ran on the actual uploaded photos
- Add photosRef kept in sync with draft.photos on every render; the
  cleanup effect reads photosRef.current at unmount time so it always
  sees the full, current photo list regardless of when they were uploaded

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@codesandbox
Copy link
Copy Markdown

codesandbox Bot commented May 21, 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 21, 2026

Warning

Rate limit exceeded

@amoomustakim-hue has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 10 minutes and 18 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, 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 the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: d6e33d9c-0804-4d04-ac8b-fd2085055550

📥 Commits

Reviewing files that changed from the base of the PR and between 708982a and 5b4290e.

📒 Files selected for processing (3)
  • apps/web/src/app/(store)/store/products/new/_components/ScreenOnePhotos.tsx
  • apps/web/src/app/(store)/store/products/new/_components/ScreenTwoDetails.tsx
  • apps/web/src/app/(store)/store/products/new/_components/StepProgress.tsx
📝 Walkthrough

Walkthrough

This PR introduces a multi-step product listing wizard for store product creation. It includes a photo upload interface with concurrent processing and moderation status mapping, a product details form with validation and reusable components, centralized types and utilities for the wizard flow, and page-level state management with progress indication. The implementation covers steps 1–2 fully and includes a placeholder for steps 3–4.

Changes

Product Listing Wizard

Layer / File(s) Summary
Data models and upload API
apps/web/src/lib/product-listing.ts
Exports upload result types, API error handling, uploadProductImage() function that POSTs to /upload/image, category constants, and wizard state shape types (UploadedPhoto, ProductListingDetails, ProductListingDraft). Includes helpers: emptyDetails() to initialize empty product details and usableImageCount() to count approved/sensitive photos.
Photo upload with validation and concurrent processing
apps/web/src/app/(store)/store/products/new/_components/ScreenOnePhotos.tsx
Implements photo upload screen with drag-and-drop (desktop) and camera/gallery buttons (mobile), validates MIME type and file size, enforces max 5 photos, and creates preview URLs. Concurrently uploads each pending file via uploadProductImage, maps moderation results to UI status (approved/sensitive/rejected), stores cloud URLs and public IDs, and shows per-photo error messages on failure. Supports reordering (move left/right), removing (with preview URL revocation), and retrying failed uploads. Blocks "Next" until at least one usable image exists.
Product details form with validation and subcomponents
apps/web/src/app/(store)/store/products/new/_components/ScreenTwoDetails.tsx
Implements product details screen with validation logic (required name/description/category, length limits). Defines reusable form subcomponents: StyledSelect (dropdown with error state), StyledTextarea (with live character counter), TagInput (add/remove up to 3 tags via Enter/comma), SpecsAccordion (manage up to 20 key-value specs with expand/collapse), and Toggle (boolean switch). Main component conditionally shows sub-category selector based on category support and clears sub-category when switching categories. Blocks progression until validation passes.
Wizard page state and step progress indicator
apps/web/src/app/(store)/store/products/new/page.tsx, apps/web/src/app/(store)/store/products/new/_components/StepProgress.tsx
Implements NewProductPage managing multi-step wizard state (step counter, draft with photos/details). Revokes preview URLs on unmount via photosRef. Provides setPhotos helper for functional state updates. Renders conditional step content: step 1 (photos), step 2 (details), step 3+ (placeholder). Includes StepProgress component showing current step counter and visual progress bar with segment styling.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant ScreenOnePhotos
  participant uploadProductImage
  participant ImageServer
  participant ScreenTwoDetails
  participant NewProductPage
  
  User->>NewProductPage: Load wizard (Step 1)
  NewProductPage->>ScreenOnePhotos: Render with empty photos array
  
  User->>ScreenOnePhotos: Drop/select image file(s)
  ScreenOnePhotos->>ScreenOnePhotos: Validate MIME type and size
  ScreenOnePhotos->>uploadProductImage: POST file concurrently
  uploadProductImage->>ImageServer: FormData to /upload/image
  ImageServer->>uploadProductImage: UploadResult{url, moderationStatus, cloudinaryPublicId}
  uploadProductImage->>ScreenOnePhotos: Return with status mapping
  ScreenOnePhotos->>ScreenOnePhotos: Update photo status (approved/sensitive/rejected)
  ScreenOnePhotos->>User: Show preview with status indicator
  
  User->>ScreenOnePhotos: Click Next (after usable images)
  ScreenOnePhotos->>NewProductPage: onNext() callback
  NewProductPage->>NewProductPage: Increment step to 2
  NewProductPage->>ScreenTwoDetails: Render with empty details
  
  User->>ScreenTwoDetails: Enter product name, description, category, tags, specs
  User->>ScreenTwoDetails: Click Next
  ScreenTwoDetails->>ScreenTwoDetails: Validate required fields and lengths
  ScreenTwoDetails->>NewProductPage: onNext() with validated details
  NewProductPage->>NewProductPage: Increment step to 3
  NewProductPage->>User: Show placeholder (W-15b continuation)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested reviewers

  • onerandomdevv

Poem

🐰 A wizard takes shape, step by step, photo to form,
Upload, validate, reorder—each image transforms!
Details flow next, tags dance, specs align,
The product listing grows, refined and divine.
In W-15a, the foundation takes flight! ✨

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 64.29% 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 'Feat/web product listing 1 2' is vague and generic. It uses shorthand abbreviations and lacks specificity about what screens or features are being added. Use a clearer title like 'Add product listing wizard screens 1-2 with image upload and details form' to better describe the main changes.
✅ Passed checks (3 passed)
Check name Status Explanation
Description check ✅ Passed The description is comprehensive and well-structured. It follows the template with all major sections filled: what changed, type of change, testing instructions, pre-commit checklist completion, and detailed notes.
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 feat/web-product-listing-1-2

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.

onerandomdevv
onerandomdevv previously approved these changes May 21, 2026
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 (4)
apps/web/src/app/(store)/store/products/new/_components/ScreenTwoDetails.tsx (1)

547-554: 💤 Low value

Use maxLength instead of slicing on every keystroke.

<Input maxLength={MAX_SKU} /> matches the pattern used for name/description and avoids inconsistent UX where the input would otherwise grow then clamp.

🤖 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/app/`(store)/store/products/new/_components/ScreenTwoDetails.tsx
around lines 547 - 554, The SKU input currently slices the value in the onChange
handler (in ScreenTwoDetails.tsx: the Input with value={details.sku} and
onChange={(e) => set("sku", e.target.value.slice(0, MAX_SKU))}); change this to
use the Input prop maxLength={MAX_SKU} and update the onChange to simply
set("sku", e.target.value) so the browser enforces the limit and UX matches the
name/description inputs; keep value={details.sku} and remove the slicing logic.
apps/web/src/app/(store)/store/products/new/_components/ScreenOnePhotos.tsx (2)

282-291: 💤 Low value

onDragLeave will fire when dragging over child elements, causing the drop-zone state to flicker.

Using a dragenter counter or relatedTarget/contains check is the typical workaround. Not user-blocking but visible polish issue on the desktop drop zone.

🤖 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/app/`(store)/store/products/new/_components/ScreenOnePhotos.tsx
around lines 282 - 291, The drop-zone's onDragLeave handler is causing flicker
because it fires when entering child elements; update the component (where
onDragEnter, onDragLeave, setIsDragging and handleDrop are defined) to track a
drag counter or use event.relatedTarget with node.contains to determine true
leave events and only call setIsDragging(false) when the pointer has actually
left the drop-zone; increment the counter in onDragEnter, decrement in
onDragLeave (or check relatedTarget within onDragLeave against the drop-zone
element) and clear the counter/reset setIsDragging in handleDrop to ensure
stable drag state.

315-342: 💤 Low value

Imperative capture attribute toggling leaves DOM state dirty across clicks.

Tapping "Take Photo" then dismissing it leaves capture="environment" on the input until "Choose Photo" is clicked, so a subsequent drag-drop or Add more click can also inherit it. Render two separate hidden inputs (one with capture, one without) — simpler and stateless.

🤖 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/app/`(store)/store/products/new/_components/ScreenOnePhotos.tsx
around lines 315 - 342, The current ScreenOnePhotos component mutates a single
file input's capture attribute (via fileInputRef) which leaves DOM state dirty;
instead render two separate hidden file inputs — one with capture="environment"
and one without — and wire the "Take Photo" button to call click() on the
capture input (e.g., fileInputCaptureRef) and the "Choose Photo" button to call
click() on the non-capture input (e.g., fileInputRefNoCapture); remove
setAttribute/removeAttribute logic and ensure both inputs share the same change
handler (e.g., the existing onFileChange handler) so the component remains
stateless and robust across interactions.
apps/web/src/lib/product-listing.ts (1)

41-47: 💤 Low value

Consider throwing an Error subclass instead of a bare object.

Throwing plain objects loses stack traces, breaks instanceof checks, and confuses error monitoring (Sentry etc.). A small UploadError extends Error class would let callers do instanceof filtering and still expose message/code fields without changing the public type shape much.

🤖 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/lib/product-listing.ts` around lines 41 - 47, Replace the bare
object throw in uploadProductImage with a proper Error subclass: define an
UploadError extends Error that accepts message and code (and sets this.code and
this.name, ensuring Error prototype is correctly set), then throw new
UploadError("API URL is not configured. Set NEXT_PUBLIC_API_URL.",
"API_NOT_CONFIGURED") instead of the plain object; keep the thrown shape
compatible with UploadApiError by exposing the same message and code properties
so callers can still expect UploadResult/UploadApiError behavior while
preserving stack traces and enabling instanceof checks.
🤖 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/products/new/_components/ScreenOnePhotos.tsx:
- Around line 90-100: The current logic only sets fileError when all selected
files are invalid; change it so any partial rejection sets the user-facing
error. After computing validFiles and rejected in the ScreenOnePhotos component,
call setFileError("Only JPG, PNG or WebP images under 5 MB are accepted.")
whenever rejected > 0 (and clear it when rejected === 0), then if
validFiles.length === 0 return early as before; keep references to
ACCEPTED_MIME, MAX_SIZE_MB, validFiles, rejected and setFileError to locate and
update the code.
- Around line 86-113: processFiles can race because it computes canAdd from
photosRef.current before calling setPhotos; instead enforce the MAX_PHOTOS cap
inside the functional state updater you pass to setPhotos so prev is
authoritative. Move the logic that limits pendingPhotos/toUpload into the
setPhotos((prev) => { const available = MAX_PHOTOS - prev.length; const allowed
= pendingPhotos.slice(0, available); return [...prev, ...allowed]; }) pattern,
and set fileError or drop the rest based on availability; keep using
crypto.randomUUID(), previewUrl creation, and the same UploadedPhoto shape, but
do NOT rely on photosRef.current to decide how many to append. Ensure subsequent
upload logic iterates only over the actually appended photos.

In
`@apps/web/src/app/`(store)/store/products/new/_components/ScreenTwoDetails.tsx:
- Around line 67-99: This component's <label> isn't associated with the form
control; generate a stable id using React's useId() at the top of the component
(e.g., const id = useId()), set the label's htmlFor={id}, and add id={id} to the
<select> (and mirror the same pattern in StyledTextarea: add useId(), htmlFor on
the <label> and id on the <textarea>); ensure you import useId from React and
preserve existing props (value, onChange, placeholder, options, error) when
wiring the id so behavior doesn't change.
- Around line 29-46: This component currently uses a local DetailsErrors type
and the hand-rolled validateDetails(details: ProductListingDetails) logic with
MAX_NAME/MAX_DESC; refactor it to use React Hook Form with a Zod schema: create
a detailsSchema (centralizing MAX_NAME/MAX_DESC) and initialize useForm({
resolver: zodResolver(detailsSchema) }) in the ScreenTwoDetails component,
replace local useState validation and calls to validateDetails with form
registration (register) and formState.errors, and update the input components to
derive constraints/messages from the schema (or formState) instead of
duplicating MAX_NAME/MAX_DESC; remove the validateDetails function and
DetailsErrors usage.

In `@apps/web/src/app/`(store)/store/products/new/_components/StepProgress.tsx:
- Around line 20-32: Wrap the visual progress bar div (the container that maps
Array.from({ length: total }) — inside the StepProgress component) with proper
ARIA by adding role="progressbar" and the attributes aria-valuenow={step},
aria-valuemin={1}, aria-valuemax={total} and an aria-label (e.g., "Form
progress") on that same element so assistive tech can read the current step;
ensure the values are numbers/expressions (not strings) and keep the existing
visual markup unchanged.

---

Nitpick comments:
In `@apps/web/src/app/`(store)/store/products/new/_components/ScreenOnePhotos.tsx:
- Around line 282-291: The drop-zone's onDragLeave handler is causing flicker
because it fires when entering child elements; update the component (where
onDragEnter, onDragLeave, setIsDragging and handleDrop are defined) to track a
drag counter or use event.relatedTarget with node.contains to determine true
leave events and only call setIsDragging(false) when the pointer has actually
left the drop-zone; increment the counter in onDragEnter, decrement in
onDragLeave (or check relatedTarget within onDragLeave against the drop-zone
element) and clear the counter/reset setIsDragging in handleDrop to ensure
stable drag state.
- Around line 315-342: The current ScreenOnePhotos component mutates a single
file input's capture attribute (via fileInputRef) which leaves DOM state dirty;
instead render two separate hidden file inputs — one with capture="environment"
and one without — and wire the "Take Photo" button to call click() on the
capture input (e.g., fileInputCaptureRef) and the "Choose Photo" button to call
click() on the non-capture input (e.g., fileInputRefNoCapture); remove
setAttribute/removeAttribute logic and ensure both inputs share the same change
handler (e.g., the existing onFileChange handler) so the component remains
stateless and robust across interactions.

In
`@apps/web/src/app/`(store)/store/products/new/_components/ScreenTwoDetails.tsx:
- Around line 547-554: The SKU input currently slices the value in the onChange
handler (in ScreenTwoDetails.tsx: the Input with value={details.sku} and
onChange={(e) => set("sku", e.target.value.slice(0, MAX_SKU))}); change this to
use the Input prop maxLength={MAX_SKU} and update the onChange to simply
set("sku", e.target.value) so the browser enforces the limit and UX matches the
name/description inputs; keep value={details.sku} and remove the slicing logic.

In `@apps/web/src/lib/product-listing.ts`:
- Around line 41-47: Replace the bare object throw in uploadProductImage with a
proper Error subclass: define an UploadError extends Error that accepts message
and code (and sets this.code and this.name, ensuring Error prototype is
correctly set), then throw new UploadError("API URL is not configured. Set
NEXT_PUBLIC_API_URL.", "API_NOT_CONFIGURED") instead of the plain object; keep
the thrown shape compatible with UploadApiError by exposing the same message and
code properties so callers can still expect UploadResult/UploadApiError behavior
while preserving stack traces and enabling instanceof checks.
🪄 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: 8c10fd61-aa90-45a4-87ec-af8121ce9bae

📥 Commits

Reviewing files that changed from the base of the PR and between 49764d6 and 708982a.

📒 Files selected for processing (5)
  • apps/web/src/app/(store)/store/products/new/_components/ScreenOnePhotos.tsx
  • apps/web/src/app/(store)/store/products/new/_components/ScreenTwoDetails.tsx
  • apps/web/src/app/(store)/store/products/new/_components/StepProgress.tsx
  • apps/web/src/app/(store)/store/products/new/page.tsx
  • apps/web/src/lib/product-listing.ts

Comment thread apps/web/src/app/(store)/store/products/new/_components/ScreenOnePhotos.tsx Outdated
Comment on lines +29 to +46
interface DetailsErrors {
name?: string;
description?: string;
category?: string;
}

function validateDetails(details: ProductListingDetails): DetailsErrors {
const errors: DetailsErrors = {};
if (!details.name.trim()) errors.name = "Product name is required.";
else if (details.name.length > MAX_NAME)
errors.name = `Max ${MAX_NAME} characters.`;
if (!details.description.trim())
errors.description = "Description is required.";
else if (details.description.length > MAX_DESC)
errors.description = `Max ${MAX_DESC} characters.`;
if (!details.category) errors.category = "Select a platform category.";
return errors;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major | 🏗️ Heavy lift

Form validation deviates from the repo's RHF + Zod standard.

Per coding guidelines, frontend form validation must use Zod schemas and forms should use React Hook Form. This screen uses local useState<DetailsErrors> plus a hand-rolled validateDetails. Refactoring to useForm({ resolver: zodResolver(detailsSchema) }) would also centralize the field-length constants in one schema (which the input components could read from instead of duplicating).

As per coding guidelines: "All frontend form validation must use Zod schemas" and "Use React Hook Form with Zod for form validation".

Also applies to: 442-470

🤖 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/app/`(store)/store/products/new/_components/ScreenTwoDetails.tsx
around lines 29 - 46, This component currently uses a local DetailsErrors type
and the hand-rolled validateDetails(details: ProductListingDetails) logic with
MAX_NAME/MAX_DESC; refactor it to use React Hook Form with a Zod schema: create
a detailsSchema (centralizing MAX_NAME/MAX_DESC) and initialize useForm({
resolver: zodResolver(detailsSchema) }) in the ScreenTwoDetails component,
replace local useState validation and calls to validateDetails with form
registration (register) and formState.errors, and update the input components to
derive constraints/messages from the schema (or formState) instead of
duplicating MAX_NAME/MAX_DESC; remove the validateDetails function and
DetailsErrors usage.

Comment thread apps/web/src/app/(store)/store/products/new/_components/StepProgress.tsx Outdated
ScreenOnePhotos.tsx:
- Race condition: enforce MAX_PHOTOS cap inside the setPhotos functional
  setter (prev.length is authoritative) in addition to the pre-slice;
  guards against near-simultaneous processFiles calls
- Partial rejections: show fileError whenever ANY file fails validation
  (not only when every file fails); valid files still continue uploading

ScreenTwoDetails.tsx:
- Accessibility: add useId() to StyledSelect, StyledTextarea, and TagInput
  so every <label> has a matching htmlFor and every control has a matching id
- Wire aria-describedby on each control to its error <p id>

StepProgress.tsx:
- Accessibility: add role="progressbar" with aria-valuenow, aria-valuemin,
  aria-valuemax, and aria-label so screen readers announce wizard progress

Deferred to W-15b: RHF+Zod refactor for Screen 2 (form does not call
POST /products in W-15a; refactor adds scope without benefit until then)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Extract labels[step - 1] into currentLabel with a ?? '' fallback so
out-of-bounds step values never produce undefined in the rendered label
or the aria-label string.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@onerandomdevv onerandomdevv merged commit 1dd3493 into dev May 21, 2026
8 checks passed
@onerandomdevv onerandomdevv deleted the feat/web-product-listing-1-2 branch May 22, 2026 02:56
@coderabbitai coderabbitai Bot mentioned this pull request Jun 2, 2026
23 tasks
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