Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/funny-dingos-accept.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@bigcommerce/catalyst-core': minor
---

Updated product and brand pages to include the number of reviews in the product data. Fixed visual spacing within product cards. Enhanced the Rating component to display the number of reviews alongside the rating. Introduced a new RatingLink component for smooth scrolling to reviews section on PDP.
1 change: 1 addition & 0 deletions core/app/[locale]/(default)/product/[slug]/page-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ const ProductQuery = graphql(
}
reviewSummary {
averageRating
numberOfReviews
}
description
...ProductOptionsFragment
Expand Down
15 changes: 9 additions & 6 deletions core/app/[locale]/(default)/product/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -556,6 +556,7 @@ export default async function Product({ params, searchParams }: Props) {
price: streamablePrices,
reviewsEnabled,
showRating,
numberOfReviews: baseProduct.reviewSummary.numberOfReviews,
subtitle: baseProduct.brand?.name,
rating: baseProduct.reviewSummary.averageRating,
accordions: streameableAccordions,
Expand Down Expand Up @@ -583,12 +584,14 @@ export default async function Product({ params, searchParams }: Props) {
/>

{showRating && (
<Reviews
productId={productId}
searchParams={searchParams}
streamableImages={streamableImages}
streamableProduct={streamableProduct}
/>
<div id="reviews">
<Reviews
productId={productId}
searchParams={searchParams}
streamableImages={streamableImages}
streamableProduct={streamableProduct}
/>
</div>
)}

<Stream
Expand Down
1 change: 1 addition & 0 deletions core/data-transformers/product-card-transformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ export const singleProductCardTransformer = (
price: pricesTransformer(product.prices, format),
subtitle: product.brand?.name ?? undefined,
rating: product.reviewSummary.averageRating,
numberOfReviews: product.reviewSummary.numberOfReviews,
inventoryMessage: getInventoryMessage(product, outOfStockMessage, showBackorderMessage),
};
};
Expand Down
20 changes: 17 additions & 3 deletions core/vibes/soul/primitives/product-card/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export interface Product {
badge?: string;
rating?: number;
inventoryMessage?: string;
numberOfReviews?: number;
}

export interface ProductCardProps {
Expand Down Expand Up @@ -58,7 +59,18 @@ export interface ProductCardProps {
* ```
*/
export function ProductCard({
product: { id, title, subtitle, badge, price, image, href, inventoryMessage, rating },
product: {
id,
title,
subtitle,
badge,
price,
image,
href,
inventoryMessage,
rating,
numberOfReviews,
},
showRating = false,
colorScheme = 'light',
className,
Expand Down Expand Up @@ -153,7 +165,9 @@ export function ProductCard({
</span>
)}
{price != null && <PriceLabel colorScheme={colorScheme} price={price} />}
{showRating && typeof rating === 'number' && rating > 0 && <Rating rating={rating} />}
{showRating && typeof rating === 'number' && rating > 0 && (
<Rating className="mb-2 mt-1" numberOfReviews={numberOfReviews} rating={rating} />
)}
<span
className={clsx(
'block text-sm font-normal',
Expand Down Expand Up @@ -185,7 +199,7 @@ export function ProductCard({
)}
</div>
{showCompare && (
<div className="mt-auto shrink-0">
<div className="ml-1 mt-auto shrink-0">
<Compare
colorScheme={colorScheme}
label={compareLabel}
Expand Down
26 changes: 22 additions & 4 deletions core/vibes/soul/primitives/rating/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { clsx } from 'clsx';
export interface Props {
showRating?: boolean;
rating: number;
numberOfReviews?: number;
showNumberOfReviews?: boolean;
className?: string;
}

Expand Down Expand Up @@ -59,7 +61,13 @@ export const Star = ({ type }: StarType) => {
);
};

export const Rating = function Rating({ showRating = true, rating, className }: Readonly<Props>) {
export const Rating = function Rating({
showRating = true,
rating,
numberOfReviews,
showNumberOfReviews = true,
className,
}: Readonly<Props>) {
const adjustedRating = Math.min(rating, 5);

const stars: Array<StarType['type']> = Array.from({ length: 5 }, (_, index) => {
Expand All @@ -76,9 +84,19 @@ export const Rating = function Rating({ showRating = true, rating, className }:
))}

{showRating && (
<span className="ml-1.5 flex h-6 min-w-6 shrink-0 items-center justify-center rounded-full border border-contrast-100 px-1 text-xs font-medium text-contrast-400">
{adjustedRating % 1 !== 0 ? adjustedRating.toFixed(1) : adjustedRating}
</span>
<div className="flex items-center gap-1">
<span className="ml-2 flex h-6 shrink-0 items-center justify-center text-xs font-semibold text-foreground">
{adjustedRating % 1 !== 0 ? adjustedRating.toFixed(1) : adjustedRating}
</span>
{showNumberOfReviews && numberOfReviews != null && (
<div className="flex items-center gap-1">
<span className="mx-1 h-4 w-px bg-contrast-200" />
<span className="text-xs text-contrast-500">
{numberOfReviews} {numberOfReviews === 1 ? 'review' : 'reviews'}
</span>
</div>
)}
</div>
)}
</div>
);
Expand Down
16 changes: 13 additions & 3 deletions core/vibes/soul/sections/product-detail/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { Stream, Streamable } from '@/vibes/soul/lib/streamable';
import { Accordion, AccordionItem } from '@/vibes/soul/primitives/accordion';
import { AnimatedUnderline } from '@/vibes/soul/primitives/animated-underline';
import { Price, PriceLabel } from '@/vibes/soul/primitives/price-label';
import { Rating } from '@/vibes/soul/primitives/rating';
import * as Skeleton from '@/vibes/soul/primitives/skeleton';
import { type Breadcrumb, Breadcrumbs } from '@/vibes/soul/sections/breadcrumbs';
import { ProductGallery } from '@/vibes/soul/sections/product-detail/product-gallery';
Expand All @@ -16,6 +15,7 @@ import {
ProductDetailFormAction,
StockDisplayData,
} from './product-detail-form';
import { RatingLink } from './rating-link';
import { Field } from './schema';

interface ProductDetailProduct {
Expand All @@ -29,6 +29,7 @@ interface ProductDetailProduct {
rating?: Streamable<number | null>;
reviewsEnabled?: boolean;
showRating?: boolean;
numberOfReviews?: number;
summary?: Streamable<string>;
description?: Streamable<string | ReactNode | null>;
accordions?: Streamable<
Expand Down Expand Up @@ -161,8 +162,17 @@ export function ProductDetail<F extends Field>({
)}
{product.showRating && (
<div className="group/product-rating">
<Stream fallback={<RatingSkeleton />} value={product.rating}>
{(rating) => <Rating rating={rating ?? 0} />}
<Stream
fallback={<RatingSkeleton />}
value={Streamable.all([product.rating, product.numberOfReviews])}
>
{([rating, numberOfReviews]) => (
<RatingLink
numberOfReviews={numberOfReviews ?? 0}
rating={rating ?? 0}
scrollTargetId="reviews"
/>
)}
</Stream>
</div>
)}
Expand Down
28 changes: 28 additions & 0 deletions core/vibes/soul/sections/product-detail/rating-link.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
'use client';

import { Rating, type Props as RatingProps } from '@/vibes/soul/primitives/rating';

interface Props extends RatingProps {
scrollTargetId: string;
}

export function RatingLink({ scrollTargetId, ...ratingProps }: Props) {
const handleClick = () => {
const element = document.getElementById(scrollTargetId);

if (element) {
element.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
};

return (
<button
aria-label="Scroll to reviews"
className="cursor-pointer text-left"
onClick={handleClick}
type="button"
>
<Rating {...ratingProps} />
</button>
);
}
4 changes: 2 additions & 2 deletions core/vibes/soul/sections/reviews/review-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -183,25 +183,25 @@ export const ReviewForm = ({
value={typeof textControl.value === 'string' ? textControl.value : ''}
/>
<Input
disabled={isAuthorDisabled}
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Forgot that disabled fields don't submit with native HTML forms 😅 This fixes that while still preventing modifications to the input.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

("this" being changing disabledreadOnly)

errors={fields.author.errors}
label={formNameLabel}
name={fields.author.name}
onBlur={authorControl.blur}
onChange={(e) => authorControl.change(e.currentTarget.value)}
onFocus={authorControl.focus}
readOnly={isAuthorDisabled}
required={fields.author.required}
type="text"
value={typeof authorControl.value === 'string' ? authorControl.value : ''}
/>
<Input
disabled={isEmailDisabled}
errors={fields.email.errors}
label={formEmailLabel}
name={fields.email.name}
onBlur={emailControl.blur}
onChange={(e) => emailControl.change(e.currentTarget.value)}
onFocus={emailControl.focus}
readOnly={isEmailDisabled}
required={fields.email.required}
type="email"
value={typeof emailControl.value === 'string' ? emailControl.value : ''}
Expand Down