feat(web): add product listing variants pricing and size guide#303
Conversation
Review or Edit in CodeSandboxOpen the branch in Web Editor • VS Code • Insiders |
📝 WalkthroughWalkthroughThis PR completes the product creation wizard (steps 2–4) by implementing a variants/pricing screen, a size guide customization and publish screen, full payload validation and building, and orchestrating the multi-step flow in the main page component. ChangesProduct Creation Wizard
Sequence DiagramsequenceDiagram
participant User as User
participant Page as NewProductPage
participant Screen3 as ScreenThreeVariantsPricing
participant Screen4 as ScreenFourSizeGuidePublish
participant Builder as buildProductCreatePayload
participant API as createProduct
participant Server as /products
User->>Page: load product wizard
Page->>Screen3: render with pricing draft
User->>Screen3: configure dimensions, variants, pricing
Screen3->>Screen3: syncVariantRows() on dimension change
Screen3->>Screen3: validatePricing() on continue
Screen3->>Page: onNext() if valid
Page->>Screen4: render with sizeGuide draft
User->>Screen4: choose sizing guide, add custom sizes
Screen4->>Screen4: validateCustomSizes() on field edit
User->>Screen4: click Publish Now
Screen4->>Page: onSubmit(true)
Page->>Builder: buildProductCreatePayload(draft, true)
Builder->>Builder: convert prices to kobo, normalize sizes
Builder-->>Page: {payload, errors}
Page->>API: createProduct(payload)
API->>Server: POST /products
Server-->>API: {id, ...}
API-->>User: redirect to /store/products
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (5)
apps/web/src/app/(store)/store/products/new/page.tsx (1)
76-90: ⚡ Quick winPrefer TanStack Query
useMutationfor the submit flow.This keeps server-state behavior centralized and aligned with the project’s React Query standard instead of manual async + local loading/error orchestration.
As per coding guidelines, "Use React Query (TanStack Query v5) for server state management with centralized queryKeys".
🤖 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/page.tsx around lines 76 - 90, The submit flow in handleSubmit currently uses manual async/try-catch and local loading/error state; refactor it to use TanStack Query's useMutation so server-state and side effects are centralized. Replace the direct call to createProduct and local setSubmitting/setSubmitErrors handling by creating a mutation (using createProduct as the mutation function) and invoke mutation.mutateAsync with the payload generated by buildProductCreatePayload; handle success by calling router.push("/store/products") in onSuccess and map errors to setSubmitErrors or use mutation.error in the UI, while keeping buildProductCreatePayload, getSubmitErrorMessage, and existing state setters for compatibility during validation and error display. Ensure the mutation is keyed appropriately per project conventions and use mutation.isLoading instead of setSubmitting to reflect publish vs draft status.apps/web/src/app/(store)/store/products/new/_components/ScreenThreeVariantsPricing.tsx (2)
45-60: 🏗️ Heavy liftUse React Query for
/stores/meinstead of imperativeuseEffectfetch.This fetch should be moved to TanStack Query with centralized query keys per project standard.
As per coding guidelines, "Use React Query (TanStack Query v5) for server state management with centralized queryKeys".
🤖 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/ScreenThreeVariantsPricing.tsx around lines 45 - 60, Replace the imperative useEffect/api.get call in the ScreenThreeVariantsPricing component with a TanStack useQuery that fetches "/stores/me" using the centralized query key (e.g., queryKeys.store.me); update the logic that setsSupportsDropship to derive its value from the query result (setSupportsDropship(data?.storeType === "PHYSICAL" or false) or set local state from useEffect on query success), handle errors via the query's error state (defaulting supportsDropship to false), remove the mounted cleanup logic (not needed with useQuery), and import/use the shared queryKeys and useQuery hook instead of api.get and useEffect.
95-99: 🏗️ Heavy liftMigrate pricing form validation to React Hook Form + Zod.
Validation is currently custom and split across handlers/helpers. This step should use RHF + Zod schemas.
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: 732-829
🤖 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/ScreenThreeVariantsPricing.tsx around lines 95 - 99, Migrate the pricing validation in handleContinue to React Hook Form + Zod: remove the call to validatePricing and the setErrors flow in handleContinue and instead create a Zod schema (covering pricing, supportsDropship and rules currently enforced by validatePricing), initialize useForm({ resolver: zodResolver(yourSchema), defaultValues: pricing }) in this component, replace manual field handling with RHF register/control for the pricing inputs, wire the submit to handleSubmit(props => onNext()) so onNext is only called when Zod validation passes, and delete or consolidate the validatePricing function and errors state usage to rely on formState.errors from RHF. Ensure symbols referenced are the existing handleContinue, validatePricing, setErrors, onNext, pricing and supportsDropship so you update the correct logic.apps/web/src/lib/product-listing.ts (1)
184-243: 🏗️ Heavy liftNormalize ID and boolean field names to match project TS conventions.
Several newly introduced fields violate the repo naming contract (e.g.,
idshould be...Id, booleans should start withis/has/can/should). Please align these interfaces/payload shapes for consistency before this propagates further.As per coding guidelines, "All ID field names must be suffixed with 'Id'" and "All boolean field names must be prefixed with 'is', 'has', 'can', or 'should' (e.g. isActive, hasVariants)".
Also applies to: 281-297
🤖 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 184 - 243, Update the interface field names to follow project TS conventions: rename every plain "id" field to a suffixed form like "variantId", "tierId", "customSizeId", or "productId" in the affected interfaces (VariantDraftRow, VolumePricingTier, CustomSizeRow, ProductCreateResponse) and rename boolean properties to start with is/has/can/should (e.g., isActive, hasVariants, volumePricingOpen -> isVolumePricingOpen, notSoldIndividually -> isNotSoldIndividually, allowDropship -> canAllowDropship or isAllowDropship as your chosen convention); update any related names in ProductListingPricing and ProductListingSizeGuide (dimensionOneValues/dimensionTwoValues remain) to match those new identifiers so types remain consistent across usages. Ensure you also change the corresponding property names wherever these interfaces are consumed.apps/web/src/app/(store)/store/products/new/_components/ScreenFourSizeGuidePublish.tsx (1)
425-481: 🏗️ Heavy liftUse RHF + Zod for size-guide validation instead of local validators.
This screen’s validation should be schema-based and integrated with React Hook Form per project standard.
As per coding guidelines, "All frontend form validation must use Zod schemas" and "Use React Hook Form with Zod for form validation".
🤖 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/ScreenFourSizeGuidePublish.tsx around lines 425 - 481, Replace the ad-hoc validators (validateCustomSizes, validateModelMeasurements, isOptionalNumber) with a Zod schema for ProductListingSizeGuide and wire it into React Hook Form using the zod resolver: define a schema that enforces useCustomSizes boolean, validates customSizes as an array of objects where size is nonempty and numeric fields (bustCm, waistCm, hipsCm, heightCm) are optional-but-numeric, and conditionally requires footLengthCm when isFootwear is true; validate modelHeightCm/modelBustCm/modelWaistCm/modelHipsCm as optional-but-numeric in the same schema; then remove calls to validateCustomSizes/validateModelMeasurements and instead pass the Zod schema to useForm via zodResolver to drive all validation and error messages.
🤖 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/ScreenFourSizeGuidePublish.tsx:
- Around line 478-480: The inline validator isOptionalNumber currently treats
"0" as valid but the submit payload rejects non-positive values; update
isOptionalNumber to only accept empty strings or whole numbers greater than zero
by calling parseWholeNumber(value) and ensuring the result is not null and > 0
(retain the existing trim check). Reference the isOptionalNumber function and
parseWholeNumber when making this change so the screen-level validation matches
submit-time rules.
In
`@apps/web/src/app/`(store)/store/products/new/_components/ScreenThreeVariantsPricing.tsx:
- Line 777: The validation currently always runs validateVolumePricing(pricing,
errors) and thus validates pricing.volumeTiers even when volume pricing UI is
closed; update the logic so validateVolumePricing is only invoked when
pricing.volumePricingOpen is true (and similarly guard any other volume-tier
checks between lines handling volumeTiers, e.g. the block around the other
validations at 802-828), so hidden/disabled volume-tier data is skipped and
won't block continuation; locate calls to validateVolumePricing and any direct
checks of pricing.volumeTiers and wrap them with a condition like if
(pricing.volumePricingOpen) before running the validation.
In `@apps/web/src/app/`(store)/store/products/new/page.tsx:
- Around line 106-108: The page heading in the <h1> element in page.tsx is using
the Cabinet font and weight (class names font-cabinet and font-semibold); change
it to use the Syne 700 typography token by replacing font-cabinet and
font-semibold with the Syne font and weight 700 classes (e.g., font-syne and a
700 weight class such as font-[700] or font-extrabold) so the heading uses Syne
(700) per the apps/web typography rule.
- Around line 76-94: The handleSubmit function currently allows concurrent
POSTs; add an in-flight guard so repeated clicks won’t send duplicate
createProduct calls: at the top of handleSubmit check the submitting state (or a
dedicated ref like isSubmittingRef) and return early if a request is in-flight;
set the submitting state (setSubmitting or isSubmittingRef) immediately after
payload validation and before calling createProduct, then proceed with the
try/catch/finally as-is and clear the submitting flag in finally; reference
handleSubmit, setSubmitting (or an isSubmittingRef), buildProductCreatePayload,
createProduct, setSubmitErrors, and router.push when making these changes.
In `@apps/web/src/lib/product-listing.ts`:
- Around line 449-453: The payload builder currently calls
buildVolumePricingTiers and validates volume tiers unconditionally in
buildProductCreatePayload, which causes hidden/stale tiers to block submission;
change the logic to only call buildVolumePricingTiers and add
attributes.volumePricingTiers when the product's volumePricingOpen flag is true
(check the same gating where UI exposes volume pricing), and apply the same
guard in the other similar block around the code at the second occurrence (the
block spanning the area analogous to lines 578-610) so volume tier
parsing/validation is skipped when volumePricingOpen is false.
---
Nitpick comments:
In
`@apps/web/src/app/`(store)/store/products/new/_components/ScreenFourSizeGuidePublish.tsx:
- Around line 425-481: Replace the ad-hoc validators (validateCustomSizes,
validateModelMeasurements, isOptionalNumber) with a Zod schema for
ProductListingSizeGuide and wire it into React Hook Form using the zod resolver:
define a schema that enforces useCustomSizes boolean, validates customSizes as
an array of objects where size is nonempty and numeric fields (bustCm, waistCm,
hipsCm, heightCm) are optional-but-numeric, and conditionally requires
footLengthCm when isFootwear is true; validate
modelHeightCm/modelBustCm/modelWaistCm/modelHipsCm as optional-but-numeric in
the same schema; then remove calls to
validateCustomSizes/validateModelMeasurements and instead pass the Zod schema to
useForm via zodResolver to drive all validation and error messages.
In
`@apps/web/src/app/`(store)/store/products/new/_components/ScreenThreeVariantsPricing.tsx:
- Around line 45-60: Replace the imperative useEffect/api.get call in the
ScreenThreeVariantsPricing component with a TanStack useQuery that fetches
"/stores/me" using the centralized query key (e.g., queryKeys.store.me); update
the logic that setsSupportsDropship to derive its value from the query result
(setSupportsDropship(data?.storeType === "PHYSICAL" or false) or set local state
from useEffect on query success), handle errors via the query's error state
(defaulting supportsDropship to false), remove the mounted cleanup logic (not
needed with useQuery), and import/use the shared queryKeys and useQuery hook
instead of api.get and useEffect.
- Around line 95-99: Migrate the pricing validation in handleContinue to React
Hook Form + Zod: remove the call to validatePricing and the setErrors flow in
handleContinue and instead create a Zod schema (covering pricing,
supportsDropship and rules currently enforced by validatePricing), initialize
useForm({ resolver: zodResolver(yourSchema), defaultValues: pricing }) in this
component, replace manual field handling with RHF register/control for the
pricing inputs, wire the submit to handleSubmit(props => onNext()) so onNext is
only called when Zod validation passes, and delete or consolidate the
validatePricing function and errors state usage to rely on formState.errors from
RHF. Ensure symbols referenced are the existing handleContinue, validatePricing,
setErrors, onNext, pricing and supportsDropship so you update the correct logic.
In `@apps/web/src/app/`(store)/store/products/new/page.tsx:
- Around line 76-90: The submit flow in handleSubmit currently uses manual
async/try-catch and local loading/error state; refactor it to use TanStack
Query's useMutation so server-state and side effects are centralized. Replace
the direct call to createProduct and local setSubmitting/setSubmitErrors
handling by creating a mutation (using createProduct as the mutation function)
and invoke mutation.mutateAsync with the payload generated by
buildProductCreatePayload; handle success by calling
router.push("/store/products") in onSuccess and map errors to setSubmitErrors or
use mutation.error in the UI, while keeping buildProductCreatePayload,
getSubmitErrorMessage, and existing state setters for compatibility during
validation and error display. Ensure the mutation is keyed appropriately per
project conventions and use mutation.isLoading instead of setSubmitting to
reflect publish vs draft status.
In `@apps/web/src/lib/product-listing.ts`:
- Around line 184-243: Update the interface field names to follow project TS
conventions: rename every plain "id" field to a suffixed form like "variantId",
"tierId", "customSizeId", or "productId" in the affected interfaces
(VariantDraftRow, VolumePricingTier, CustomSizeRow, ProductCreateResponse) and
rename boolean properties to start with is/has/can/should (e.g., isActive,
hasVariants, volumePricingOpen -> isVolumePricingOpen, notSoldIndividually ->
isNotSoldIndividually, allowDropship -> canAllowDropship or isAllowDropship as
your chosen convention); update any related names in ProductListingPricing and
ProductListingSizeGuide (dimensionOneValues/dimensionTwoValues remain) to match
those new identifiers so types remain consistent across usages. Ensure you also
change the corresponding property names wherever these interfaces are consumed.
🪄 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: d022639e-4680-49d3-93b7-44e93e465876
📒 Files selected for processing (4)
apps/web/src/app/(store)/store/products/new/_components/ScreenFourSizeGuidePublish.tsxapps/web/src/app/(store)/store/products/new/_components/ScreenThreeVariantsPricing.tsxapps/web/src/app/(store)/store/products/new/page.tsxapps/web/src/lib/product-listing.ts
| function isOptionalNumber(value: string): boolean { | ||
| if (!value.trim()) return true; | ||
| return parseWholeNumber(value) !== null; |
There was a problem hiding this comment.
Inline numeric validation should require positive integers to match submit-time rules.
isOptionalNumber currently accepts 0, but payload construction rejects non-positive values, so users can pass this screen and fail at submit.
Suggested fix
function isOptionalNumber(value: string): boolean {
if (!value.trim()) return true;
- return parseWholeNumber(value) !== null;
+ const parsed = parseWholeNumber(value);
+ return parsed !== null && parsed > 0;
}📝 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.
| function isOptionalNumber(value: string): boolean { | |
| if (!value.trim()) return true; | |
| return parseWholeNumber(value) !== null; | |
| function isOptionalNumber(value: string): boolean { | |
| if (!value.trim()) return true; | |
| const parsed = parseWholeNumber(value); | |
| return parsed !== null && parsed > 0; | |
| } |
🤖 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/ScreenFourSizeGuidePublish.tsx
around lines 478 - 480, The inline validator isOptionalNumber currently treats
"0" as valid but the submit payload rejects non-positive values; update
isOptionalNumber to only accept empty strings or whole numbers greater than zero
by calling parseWholeNumber(value) and ensuring the result is not null and > 0
(retain the existing trim check). Reference the isOptionalNumber function and
parseWholeNumber when making this change so the screen-level validation matches
submit-time rules.
| errors.push("Stock must be a whole number."); | ||
| } | ||
|
|
||
| validateVolumePricing(pricing, errors); |
There was a problem hiding this comment.
Skip volume-tier validation when volume pricing is disabled.
You currently validate volumeTiers even when pricing.volumePricingOpen is false, which can block continue on hidden data.
Suggested fix
- validateVolumePricing(pricing, errors);
+ if (pricing.volumePricingOpen) {
+ validateVolumePricing(pricing, errors);
+ }Also applies to: 802-828
🤖 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/ScreenThreeVariantsPricing.tsx
at line 777, The validation currently always runs validateVolumePricing(pricing,
errors) and thus validates pricing.volumeTiers even when volume pricing UI is
closed; update the logic so validateVolumePricing is only invoked when
pricing.volumePricingOpen is true (and similarly guard any other volume-tier
checks between lines handling volumeTiers, e.g. the block around the other
validations at 802-828), so hidden/disabled volume-tier data is skipped and
won't block continuation; locate calls to validateVolumePricing and any direct
checks of pricing.volumeTiers and wrap them with a condition like if
(pricing.volumePricingOpen) before running the validation.
| const handleSubmit = async (publishNow: boolean) => { | ||
| setSubmitErrors([]); | ||
| const result = buildProductCreatePayload(draft, publishNow); | ||
|
|
||
| if (!result.payload) { | ||
| setSubmitErrors(result.errors); | ||
| return; | ||
| } | ||
|
|
||
| setSubmitting(publishNow ? "publish" : "draft"); | ||
| try { | ||
| await createProduct(result.payload); | ||
| router.push("/store/products"); | ||
| } catch (error) { | ||
| setSubmitErrors([getSubmitErrorMessage(error)]); | ||
| } finally { | ||
| setSubmitting(null); | ||
| } | ||
| }; |
There was a problem hiding this comment.
Add an in-flight guard to prevent duplicate product creation requests.
createProduct is a non-idempotent POST. Rapid repeated submits can dispatch duplicate creates before UI disable state settles.
Suggested fix
const photosRef = useRef(draft.photos);
+ const submitInFlightRef = useRef(false);
photosRef.current = draft.photos;
@@
const handleSubmit = async (publishNow: boolean) => {
+ if (submitInFlightRef.current) return;
setSubmitErrors([]);
const result = buildProductCreatePayload(draft, publishNow);
@@
+ submitInFlightRef.current = true;
setSubmitting(publishNow ? "publish" : "draft");
try {
await createProduct(result.payload);
router.push("/store/products");
@@
} finally {
+ submitInFlightRef.current = false;
setSubmitting(null);
}
};🤖 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/page.tsx around lines 76 - 94,
The handleSubmit function currently allows concurrent POSTs; add an in-flight
guard so repeated clicks won’t send duplicate createProduct calls: at the top of
handleSubmit check the submitting state (or a dedicated ref like
isSubmittingRef) and return early if a request is in-flight; set the submitting
state (setSubmitting or isSubmittingRef) immediately after payload validation
and before calling createProduct, then proceed with the try/catch/finally as-is
and clear the submitting flag in finally; reference handleSubmit, setSubmitting
(or an isSubmittingRef), buildProductCreatePayload, createProduct,
setSubmitErrors, and router.push when making these changes.
| <h1 className="font-cabinet text-base font-semibold text-[var(--color-bianca)]"> | ||
| Add product | ||
| </h1> |
There was a problem hiding this comment.
Use Syne (700) for the page heading instead of Cabinet.
This heading violates the typography token rule for apps/web/src.
Suggested fix
- <h1 className="font-cabinet text-base font-semibold text-[var(--color-bianca)]">
+ <h1 className="font-syne text-base font-bold text-[var(--color-bianca)]">
Add product
</h1>As per coding guidelines, "Use Syne font (weight 700) for all headings via next/font/google".
📝 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.
| <h1 className="font-cabinet text-base font-semibold text-[var(--color-bianca)]"> | |
| Add product | |
| </h1> | |
| <h1 className="font-syne text-base font-bold text-[var(--color-bianca)]"> | |
| Add product | |
| </h1> |
🤖 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/page.tsx around lines 106 - 108,
The page heading in the <h1> element in page.tsx is using the Cabinet font and
weight (class names font-cabinet and font-semibold); change it to use the Syne
700 typography token by replacing font-cabinet and font-semibold with the Syne
font and weight 700 classes (e.g., font-syne and a 700 weight class such as
font-[700] or font-extrabold) so the heading uses Syne (700) per the apps/web
typography rule.
| const attributes: Record<string, unknown> = {}; | ||
| const volumePricingTiers = buildVolumePricingTiers(pricing, errors); | ||
| if (volumePricingTiers.length > 0) { | ||
| attributes.volumePricingTiers = volumePricingTiers; | ||
| } |
There was a problem hiding this comment.
Gate volume-tier parsing by volumePricingOpen to avoid false validation failures.
buildProductCreatePayload validates/builds volume tiers even when volume pricing is off, so stale hidden tier values can still block submission.
Suggested fix
- const volumePricingTiers = buildVolumePricingTiers(pricing, errors);
+ const volumePricingTiers = pricing.volumePricingOpen
+ ? buildVolumePricingTiers(pricing, errors)
+ : [];Also applies to: 578-610
🤖 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 449 - 453, The payload
builder currently calls buildVolumePricingTiers and validates volume tiers
unconditionally in buildProductCreatePayload, which causes hidden/stale tiers to
block submission; change the logic to only call buildVolumePricingTiers and add
attributes.volumePricingTiers when the product's volumePricingOpen flag is true
(check the same gating where UI exposes volume pricing), and apply the same
guard in the other similar block around the code at the second occurrence (the
block spanning the area analogous to lines 578-610) so volume tier
parsing/validation is skipped when volumePricingOpen is false.
What does this PR do?
This PR implements W-15b: Product Listing Form Screens 3 + 4. It continues the existing product listing wizard from W-15a by adding variant generation, pricing, stock input, volume tiers, not-sold-individually rules, physical-store dropship gating, size guide preview, custom size rows, model measurements, and final Publish Now / Save as Draft submission. The payload helper maps the form data to the current B-11/B-12 backend product contract and submits through
POST /products.Type of change
Area affected
How to test this
Checkout the branch:
git checkout feat/web-product-listing-3-4```Run web checks:
cd apps/web
pnpm run lint
npx tsc --noEmit
pnpm run build
Test the product listing wizard:
Expected result: Screens 3 and 4 work inside the existing product listing wizard, generated variants map correctly to stock/pricing payloads, money values are sent as integer kobo, no fabricated stock defaults are used, and successful publish/draft redirects to /store/products.
Notes for reviewer
Implemented:
No backend contract mismatch was found.
No backend or schema changes were needed.
Summary by CodeRabbit