Skip to content

Commit a5476a4

Browse files
authored
Sale price display updates
* Tweaks to the display of sale/discount pricing. * Added sale price display for bundle component. * Switched all display styles to strikePriceWithCalcValue * Added support for featured product sale pricing and refactored a bit. * Fixed build issue related to refactoring. * Code review fixes. * Tweaks to enable price bolding only when there is an active sale price.
1 parent f9772e9 commit a5476a4

File tree

12 files changed

+399
-70
lines changed

12 files changed

+399
-70
lines changed

examples/commerce-essentials/src/app/(store)/cart/CartItem.tsx

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import Link from "next/link";
66
import { CartItem as CartItemType } from "@elasticpath/js-sdk";
77
import { LoadingDots } from "../../../components/LoadingDots";
88
import { getProductURLSegment } from "../../../lib/product-helper";
9+
import PriceDisplay, { SalePriceDisplayStyle } from "../../../components/product/PriceDisplay";
910

1011
export type CartItemProps = {
1112
item: CartItemType;
@@ -15,6 +16,10 @@ export type CartItemProps = {
1516
export function CartItem({ item, productSlug }: CartItemProps) {
1617
const { mutate, isPending } = useCartRemoveItem();
1718
const canonicalURL = getProductURLSegment({ id: item.product_id, attributes: { slug: productSlug } });
19+
const display_original_price =
20+
item.meta.display_price.without_discount?.value.amount &&
21+
item.meta.display_price.without_discount?.value.amount !==
22+
item.meta.display_price.with_tax.value.amount;
1823
return (
1924
<div className="flex gap-5">
2025
<div className="flex w-16 sm:w-24 h-20 sm:h-[7.5rem] justify-center shrink-0 items-start">
@@ -31,16 +36,17 @@ export function CartItem({ item, productSlug }: CartItemProps) {
3136
</span>
3237
</div>
3338
<div className="flex h-7 gap-2 flex-col">
34-
<span className="font-medium">
35-
{item.meta.display_price.with_tax.value.formatted}
36-
</span>
37-
{item.meta.display_price.without_discount?.value.amount &&
38-
item.meta.display_price.without_discount?.value.amount !==
39-
item.meta.display_price.with_tax.value.amount && (
40-
<span className="text-black/60 text-sm line-through">
41-
{item.meta.display_price.without_discount?.value.formatted}
42-
</span>
43-
)}
39+
<PriceDisplay
40+
display_price={item.meta.display_price.with_tax.value}
41+
original_display_price={
42+
display_original_price &&
43+
item.meta.display_price.without_discount?.value
44+
}
45+
salePriceDisplay={SalePriceDisplayStyle.strikePriceWithCalcValue}
46+
showCurrency={false}
47+
priceDisplayStyleOverride="text-xl text-gray-900"
48+
saleCalcDisplayStyleOverride="pr-4 text-sm font-light text-red-500 content-center"
49+
/>
4450
</div>
4551
</div>
4652
<div className="flex w-[15rem] gap-5 items-center">

examples/commerce-essentials/src/app/(store)/cart/CartItemWide.tsx

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,14 @@ import { NumberInput } from "../../../components/number-input/NumberInput";
66
import { CartItemProps } from "./CartItem";
77
import { LoadingDots } from "../../../components/LoadingDots";
88
import { getProductURLSegment } from "../../../lib/product-helper";
9+
import PriceDisplay, { SalePriceDisplayStyle } from "../../../components/product/PriceDisplay";
910

1011
export function CartItemWide({ item, productSlug }: CartItemProps) {
1112
const { mutate, isPending } = useCartRemoveItem();
1213
const canonicalURL = getProductURLSegment({ id: item.product_id, attributes: { slug: productSlug } });
14+
const display_original_price = item.meta.display_price.without_discount?.value.amount &&
15+
item.meta.display_price.without_discount?.value.amount !==
16+
item.meta.display_price.with_tax.value.amount;
1317
return (
1418
<div className="flex gap-5 self-stretch">
1519
{/* Thumbnail */}
@@ -46,16 +50,15 @@ export function CartItemWide({ item, productSlug }: CartItemProps) {
4650
</div>
4751
</div>
4852
<div className="flex lg:pl-14 flex-col h-7 items-end">
49-
<span className="font-medium">
50-
{item.meta.display_price.with_tax.value.formatted}
51-
</span>
52-
{item.meta.display_price.without_discount?.value.amount &&
53-
item.meta.display_price.without_discount?.value.amount !==
54-
item.meta.display_price.with_tax.value.amount && (
55-
<span className="text-black/60 text-sm line-through">
56-
{item.meta.display_price.without_discount?.value.formatted}
57-
</span>
58-
)}
53+
<PriceDisplay
54+
display_price={item.meta.display_price.with_tax.value}
55+
original_display_price={
56+
display_original_price &&
57+
item.meta.display_price.without_discount?.value
58+
}
59+
showCurrency={false}
60+
salePriceDisplay={SalePriceDisplayStyle.strikePriceWithCalcValue}
61+
/>
5962
</div>
6063
</div>
6164
);

examples/commerce-essentials/src/components/checkout-item/CheckoutItem.tsx

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
"use client";
2-
import { ProductThumbnail } from "../../app/(store)/account/orders/[orderId]/ProductThumbnail";
3-
import Link from "next/link";
42
import { CartItem } from "@elasticpath/js-sdk";
3+
import Link from "next/link";
4+
import { ProductThumbnail } from "../../app/(store)/account/orders/[orderId]/ProductThumbnail";
55
import { getProductURLSegment } from "../../lib/product-helper";
6+
import PriceDisplay, { SalePriceDisplayStyle } from "../product/PriceDisplay";
67

78
export function CheckoutItem({ item, productSlug }: { item: CartItem, productSlug?: string }) {
89
const canonicalURL = getProductURLSegment({ id: item.product_id, attributes: { slug: productSlug } });
10+
const display_original_price =
11+
item.meta.display_price.without_discount?.value.amount &&
12+
item.meta.display_price.without_discount?.value.amount !==
13+
item.meta.display_price.with_tax.value.amount;
914
return (
1015
<div className="flex w-full lg:w-[24.375rem] gap-5 items-start">
1116
<div className="flex flex-col w-[4.5rem] h-[5.626rem] justify-start shrink-0 items-center">
@@ -18,16 +23,15 @@ export function CheckoutItem({ item, productSlug }: { item: CartItem, productSlu
1823
<span className="text-sm text-black/60">Quantity: {item.quantity}</span>
1924
</div>
2025
<div className="flex flex-col items-center gap-2">
21-
<span className="font-medium">
22-
{item.meta.display_price.with_tax.value.formatted}
23-
</span>
24-
{item.meta.display_price.without_discount?.value.amount &&
25-
item.meta.display_price.without_discount?.value.amount !==
26-
item.meta.display_price.with_tax.value.amount && (
27-
<span className="text-black/60 text-sm line-through">
28-
{item.meta.display_price.without_discount?.value.formatted}
29-
</span>
30-
)}
26+
<PriceDisplay
27+
display_price={item.meta.display_price.with_tax.value}
28+
original_display_price={
29+
display_original_price &&
30+
item.meta.display_price.without_discount?.value
31+
}
32+
showCurrency={false}
33+
salePriceDisplay={SalePriceDisplayStyle.strikePriceWithCalcValue}
34+
/>
3135
</div>
3236
</div>
3337
);

examples/commerce-essentials/src/components/featured-products/FeaturedProducts.tsx

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
"use server";
2-
import clsx from "clsx";
3-
import Link from "next/link";
42
import { ArrowRightIcon, EyeSlashIcon } from "@heroicons/react/24/outline";
3+
import clsx from "clsx";
54
import Image from "next/image";
5+
import Link from "next/link";
66
import { getServerSideImplicitClient } from "../../lib/epcc-server-side-implicit-client";
7+
import { getProductDisplayPrices, getProductURLSegment } from "../../lib/product-helper";
8+
import PriceDisplay, { SalePriceDisplayStyle } from "../product/PriceDisplay";
79
import { fetchFeaturedProducts } from "./fetchFeaturedProducts";
8-
import { getProductURLSegment } from "../../lib/product-helper";
10+
import { FormattedPrice } from "@elasticpath/js-sdk";
911

1012
interface IFeaturedProductsProps {
1113
title: string;
@@ -21,6 +23,16 @@ export default async function FeaturedProducts({
2123
}: IFeaturedProductsProps) {
2224
const client = getServerSideImplicitClient();
2325
const products = await fetchFeaturedProducts(client);
26+
const productPriceMap = new Map<
27+
string,
28+
{
29+
displayPrice: FormattedPrice | unknown;
30+
originalPrice: FormattedPrice | unknown;
31+
}
32+
>();
33+
for (const product of products) {
34+
productPriceMap.set(product.id, getProductDisplayPrices(product));
35+
};
2436

2537
return (
2638
<div
@@ -75,9 +87,18 @@ export default async function FeaturedProducts({
7587
<p className="pointer-events-none mt-2 block truncate text-sm font-medium text-gray-900">
7688
{product.attributes.name}
7789
</p>
78-
<p className="pointer-events-none block text-sm font-medium text-gray-500">
79-
{product.meta.display_price?.without_tax?.formatted}
80-
</p>
90+
<PriceDisplay
91+
display_price={productPriceMap.get(product.id)?.displayPrice}
92+
original_display_price={
93+
productPriceMap.get(product.id)?.originalPrice
94+
}
95+
showCurrency={false}
96+
salePriceDisplay={
97+
SalePriceDisplayStyle.strikePriceWithCalcValue
98+
}
99+
priceDisplayStyleOverride="text-base text-gray-500"
100+
saleCalcDisplayStyleOverride="pr-4 text-sm font-light text-red-500 content-center"
101+
/>
81102
</li>
82103
</Link>
83104
))}

examples/commerce-essentials/src/components/product/Price.tsx

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,21 @@
1+
import clsx from "clsx";
2+
13
interface IPriceProps {
24
price: string;
35
currency: string;
4-
size?: string;
6+
styleOverride?: string;
7+
activeSalePrice?: boolean;
58
}
69

7-
const Price = ({ price, currency, size }: IPriceProps): JSX.Element => {
10+
const Price = ({
11+
price,
12+
currency,
13+
styleOverride,
14+
activeSalePrice
15+
}: IPriceProps): JSX.Element => {
816
return (
917
<span
10-
className={`mt-4 font-light text-gray-900 ${size ? size : "text-2xl"}`}
18+
className={clsx(activeSalePrice && "font-bold", styleOverride ? styleOverride : "text-2xl text-gray-900")}
1119
>
1220
{price} {currency}
1321
</span>
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import Price from "./Price";
2+
import SaleDisplay from "./SaleDisplay";
3+
import StrikePrice from "./StrikePrice";
4+
5+
interface IPriceDisplayProps {
6+
display_price: any;
7+
salePriceDisplay: SalePriceDisplayStyle;
8+
original_display_price: any;
9+
showCurrency?: boolean;
10+
priceDisplayStyleOverride?: string;
11+
saleCalcDisplayStyleOverride?: string;
12+
}
13+
14+
export enum SalePriceDisplayStyle {
15+
none = "none",
16+
strikePrice = "strike-price",
17+
strikePriceWithCalcValue = "strike-price-with-calc-value",
18+
strikePriceWithCalcPercent = "strike-price-with-calc-percent"
19+
}
20+
21+
const PriceDisplay = ({
22+
display_price,
23+
original_display_price,
24+
salePriceDisplay,
25+
showCurrency,
26+
priceDisplayStyleOverride,
27+
saleCalcDisplayStyleOverride,
28+
}: IPriceDisplayProps): JSX.Element => {
29+
const currentPrice = (
30+
<Price
31+
price={display_price?.formatted}
32+
currency={showCurrency ? display_price?.currency : ""}
33+
styleOverride={priceDisplayStyleOverride}
34+
activeSalePrice={original_display_price && salePriceDisplay !== SalePriceDisplayStyle.none}
35+
/>
36+
);
37+
38+
const currentStrikePrice = original_display_price && (
39+
<StrikePrice
40+
price={original_display_price.formatted}
41+
currency={showCurrency ? original_display_price.currency : ""}
42+
/>
43+
);
44+
45+
let displayValue;
46+
if (
47+
original_display_price &&
48+
salePriceDisplay === SalePriceDisplayStyle.strikePriceWithCalcValue
49+
) {
50+
const amountOff =
51+
(original_display_price.amount - display_price.amount) / 100;
52+
const amountOffDisplay =
53+
"-" +
54+
new Intl.NumberFormat("en", {
55+
style: "currency",
56+
currency: display_price.currency,
57+
trailingZeroDisplay: "stripIfInteger",
58+
}).format(amountOff);
59+
displayValue = (
60+
<SaleDisplay
61+
value={amountOffDisplay}
62+
styleOverride={saleCalcDisplayStyleOverride}
63+
/>
64+
);
65+
} else if (
66+
original_display_price &&
67+
salePriceDisplay === SalePriceDisplayStyle.strikePriceWithCalcPercent
68+
) {
69+
const amountOff = original_display_price.amount - display_price.amount;
70+
const percentOff = amountOff / original_display_price.amount;
71+
const percentOffDisplay =
72+
"-" +
73+
new Intl.NumberFormat("en", {
74+
style: "percent",
75+
roundingMode: "trunc",
76+
}).format(percentOff);
77+
displayValue = (
78+
<SaleDisplay
79+
value={percentOffDisplay}
80+
styleOverride={saleCalcDisplayStyleOverride}
81+
/>
82+
);
83+
}
84+
return (
85+
<div className="flex flex-col mt-4 items-end">
86+
{salePriceDisplay === SalePriceDisplayStyle.none && currentPrice}
87+
{salePriceDisplay === SalePriceDisplayStyle.strikePrice && (
88+
<>
89+
{currentPrice}
90+
{currentStrikePrice}
91+
</>
92+
)}
93+
{(salePriceDisplay === SalePriceDisplayStyle.strikePriceWithCalcValue ||
94+
salePriceDisplay ===
95+
SalePriceDisplayStyle.strikePriceWithCalcPercent) && (
96+
<>
97+
<div className="flex flex-row">
98+
{displayValue}
99+
{currentPrice}
100+
</div>
101+
{currentStrikePrice}
102+
</>
103+
)}
104+
</div>
105+
);
106+
};
107+
108+
export default PriceDisplay;

examples/commerce-essentials/src/components/product/ProductSummary.tsx

Lines changed: 16 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
import { useContext } from "react";
2-
import Price from "./Price";
3-
import StrikePrice from "./StrikePrice";
4-
import clsx from "clsx";
51
import type { ShopperProduct } from "@elasticpath/react-shopper-hooks";
2+
import clsx from "clsx";
3+
import { useContext } from "react";
64
import { ProductContext } from "../../lib/product-context";
5+
import PriceDisplay, { SalePriceDisplayStyle } from "./PriceDisplay";
6+
import { getProductDisplayPrices } from "../../lib/product-helper";
7+
import { ProductResponse } from "@elasticpath/js-sdk";
78

89
interface IProductSummary {
910
product: ShopperProduct["response"];
@@ -12,32 +13,26 @@ interface IProductSummary {
1213
const ProductSummary = ({ product }: IProductSummary): JSX.Element => {
1314
const {
1415
attributes,
15-
meta: { display_price, original_display_price },
1616
} = product;
1717
const context = useContext(ProductContext);
18-
18+
const { displayPrice, originalPrice } = getProductDisplayPrices(
19+
product as ProductResponse,
20+
);
1921
return (
2022
<div
2123
className={clsx(context?.isChangingSku && "opacity-20 cursor-default")}
2224
>
2325
<span className="text-xl font-semibold leading-[1.1] sm:text-3xl lg:text-4xl">
2426
{attributes.name}
2527
</span>
26-
{display_price && (
27-
<div className="flex items-center">
28-
<Price
29-
price={display_price.without_tax.formatted}
30-
currency={display_price.without_tax.currency}
31-
size="text-2xl"
32-
/>
33-
{original_display_price && (
34-
<StrikePrice
35-
price={original_display_price.without_tax.formatted}
36-
currency={original_display_price.without_tax.currency}
37-
/>
38-
)}
39-
</div>
40-
)}
28+
<div className="flex items-center">
29+
<PriceDisplay
30+
display_price={displayPrice}
31+
original_display_price={originalPrice}
32+
showCurrency={false}
33+
salePriceDisplay={SalePriceDisplayStyle.strikePriceWithCalcValue}
34+
/>
35+
</div>
4136
</div>
4237
);
4338
};

0 commit comments

Comments
 (0)