From 56fcce35c895c8a327ca493eac73a737ef5780e9 Mon Sep 17 00:00:00 2001 From: fazli Date: Sat, 18 Jul 2026 13:19:27 +0000 Subject: [PATCH] feat(product-detail): ProductDescription section + strict discount guard + category types MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - app/components/product/ProductDescription.tsx: new below-fold section with a "توضیحات محصول" heading, whitespace-pre-line so multi-line seller copy (Instagram scrapes, victorwear WooCommerce imports) renders with its bullet-list line breaks preserved. Renders on both mobile and desktop; nothing shown when the product has no description. - app/components/product/ProductInfo.tsx: removed the collapsed single-

mobile description; the new section replaces it. - app/components/product/ProductDetailView.tsx: pricingOf now hides the discount when discount_price >= price (was only hiding when equal), so a stale row where the discount price accidentally matches or exceeds the base can never render as a fake promo. Mounts ProductDescription just above the mobile Contact button and reviews. - app/components/product/index.ts: export ProductDescription. - src/api/types/models/index.ts: ProductCategory now carries readonly parentId / parentName so the two-step category picker can build the parent→sub tree from a flat category list without an extra fetch. Co-Authored-By: Claude Opus 4.7 --- app/components/product/ProductDescription.tsx | 33 +++++++++++++++++++ app/components/product/ProductDetailView.tsx | 11 +++++-- app/components/product/ProductInfo.tsx | 4 ++- app/components/product/index.ts | 1 + src/api/types/models/index.ts | 18 ++++++++-- 5 files changed, 61 insertions(+), 6 deletions(-) create mode 100644 app/components/product/ProductDescription.tsx diff --git a/app/components/product/ProductDescription.tsx b/app/components/product/ProductDescription.tsx new file mode 100644 index 0000000..e6c9059 --- /dev/null +++ b/app/components/product/ProductDescription.tsx @@ -0,0 +1,33 @@ +import { AlignRight } from "lucide-react"; + +interface ProductDescriptionProps { + description?: string | null; +} + +/** + * Full product description block. Preserves the seller's line breaks (bullet + * lists from Instagram/WooCommerce imports come in as `\n`-separated lines), + * with a proper heading so it reads as its own section on the detail page. + * + * Renders nothing when no description is set — the section header shouldn't + * appear on products the seller hasn't filled out yet. + */ +export function ProductDescription({ description }: ProductDescriptionProps) { + const body = (description || "").trim(); + if (!body) return null; + + return ( +

+
+ +

توضیحات محصول

+
+
+ {body} +
+
+ ); +} diff --git a/app/components/product/ProductDetailView.tsx b/app/components/product/ProductDetailView.tsx index e0db283..d989ec2 100644 --- a/app/components/product/ProductDetailView.tsx +++ b/app/components/product/ProductDetailView.tsx @@ -3,6 +3,7 @@ import { ProductImageCarousel, ProductActions, ProductInfo, + ProductDescription, ColorSelector, SizeSelector, ContactButton, @@ -138,7 +139,12 @@ export const ProductDetailView = memo(function ProductDetailView({ discountPrice: v?.discountPrice && v.discountPrice !== "0" && - Number(v.discountPrice) !== Number(v.price) + // Only surface as a "discount" when it's a real saving. A value equal + // to (or somehow larger than) `price` is stale/mis-entered state, not a + // discount — sellers occasionally end up with matching numbers when + // they set "no discount" on edit, and rendering a strikethrough on that + // fakes a promo the seller didn't intend. + Number(v.discountPrice) < Number(v.price) ? v.discountPrice : null, }); @@ -713,9 +719,10 @@ export const ProductDetailView = memo(function ProductDetailView({ - {/* Full-width below the fold: contact, reviews, similar */} + {/* Full-width below the fold: description, contact, reviews, similar */} {productActiveStatus && ( <> + {!isOwner && (
diff --git a/app/components/product/ProductInfo.tsx b/app/components/product/ProductInfo.tsx index 061b14c..f031aee 100644 --- a/app/components/product/ProductInfo.tsx +++ b/app/components/product/ProductInfo.tsx @@ -25,6 +25,8 @@ export const ProductInfo = ({ ) : null}

{product.title}

-

{product.description}

+ {/* Description moved into the dedicated ProductDescription section below + the buy actions so multi-line seller copy renders with line breaks and + appears on desktop too. */} ); diff --git a/app/components/product/index.ts b/app/components/product/index.ts index 4d37a0f..1efbde5 100644 --- a/app/components/product/index.ts +++ b/app/components/product/index.ts @@ -2,6 +2,7 @@ export { ProductHeader } from "./ProductHeader"; export { ProductImageCarousel } from "./ProductImageCarousel"; export { ProductActions } from "./ProductActions"; export { ProductInfo } from "./ProductInfo"; +export { ProductDescription } from "./ProductDescription"; export { ColorSelector } from "./ColorSelector"; export { SizeSelector } from "./SizeSelector"; export { ContactButton } from "./ContactButton"; diff --git a/src/api/types/models/index.ts b/src/api/types/models/index.ts index 9835651..4ffe851 100644 --- a/src/api/types/models/index.ts +++ b/src/api/types/models/index.ts @@ -3508,23 +3508,35 @@ export interface ProductAttributeValue { */ export interface ProductCategory { /** - * + * * @type {string} * @memberof ProductCategory */ readonly id?: string; /** - * + * * @type {string} * @memberof ProductCategory */ name: string; /** - * + * * @type {string} * @memberof ProductCategory */ description?: string | null; + /** + * UUID of this category's parent (null for top-level parents). + * @type {string} + * @memberof ProductCategory + */ + readonly parentId?: string | null; + /** + * Human-readable name of the parent (null for top-level parents). + * @type {string} + * @memberof ProductCategory + */ + readonly parentName?: string | null; } /** *