-
Notifications
You must be signed in to change notification settings - Fork 64
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
## What's the purpose of this pull request? - This pull request adds the new molecule ReviewCard component. ## How does it work? - The ReviewCard will display the following information: - User's name - Verified user (boolean) - Review date - Review title and text - "Read more" and "Read less" options - Tooltip (displayed only for verified users when hovering over the verified icon next to the name) - The card must be **responsive** and rearrange itself for both desktop and mobile/tablet views. - Compose components were utilized to create the **ReviewAuthor** and **ReviewDate** components. - This component will be used in the listing of reviews for a product in the new **Reviews and Ratings** section. ## How to test it? <!--- Describe the steps with bullet points. Is there any external link that can be used to better test it or an example? ---> ### Starters Deploy Preview [Preview](https://starter-git-feat-add-review-card-custom-section-vtex.vercel.app/review-and-ratings-playground) <!--- Add a link to a deploy preview from `starter.store` with this branch being used. ---> <!--- Tip: You can get an installable version of this branch from the CodeSandbox generated when this PR is created. ---> ## References [JIRA TASK: SFS-2071](https://vtex-dev.atlassian.net/browse/SFS-2071) [Figma](https://www.figma.com/design/YXU6IX2htN2yg7udpCumZv/Reviews-%26-Ratings?node-id=131-64429&t=0rv1XYm6XsgBeDwA-4)   --------- Co-authored-by: Fanny Chien <fanny.chien@vtex.com> Co-authored-by: Pedro Soares <32311264+pedromtec@users.noreply.github.com>
- Loading branch information
1 parent
7379ea7
commit 5b1d946
Showing
11 changed files
with
593 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
143 changes: 143 additions & 0 deletions
143
packages/components/src/molecules/ReviewCard/ReviewCard.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,143 @@ | ||
import React, { forwardRef, useEffect, useRef, useState } from 'react' | ||
import type { HTMLAttributes } from 'react' | ||
import Rating from '../Rating' | ||
import Link from '../../atoms/Link' | ||
import ReviewCardAuthor, { | ||
type ReviewCardAuthorProps, | ||
} from './ReviewCardAuthor' | ||
import ReviewCardDate, { type ReviewCardDateProps } from './ReviewCardDate' | ||
|
||
export interface ReviewCardProps | ||
extends HTMLAttributes<HTMLDivElement>, | ||
Partial<ReviewCardAuthorProps>, | ||
Partial<ReviewCardDateProps> { | ||
/** | ||
* ID to find this component in testing tools (e.g.: cypress, testing library, and jest). | ||
*/ | ||
testId?: string | ||
/** | ||
* The title of the review. | ||
*/ | ||
title: string | ||
/** | ||
* The text of the review. | ||
*/ | ||
text: string | ||
/** | ||
* The rating of the review. | ||
*/ | ||
rating: number | ||
/** | ||
* Text to be displayed in the read more button. Defaults to 'Read More'. | ||
*/ | ||
readMoreText?: string | ||
/** | ||
* Text to be displayed in the read less button. Defaults to 'Read Less'. | ||
*/ | ||
readLessText?: string | ||
} | ||
|
||
const ReviewCard = forwardRef<HTMLDivElement, ReviewCardProps>( | ||
function ReviewCard( | ||
{ | ||
title, | ||
text, | ||
rating, | ||
author, | ||
date, | ||
locale = 'en-US', | ||
todayLabel = 'Today', | ||
isVerified, | ||
verifiedText = 'Verified User', | ||
readMoreText = 'Read More', | ||
readLessText = 'Read Less', | ||
testId = 'fs-review-card', | ||
...otherProps | ||
}, | ||
ref | ||
) { | ||
const [isExpanded, setIsExpanded] = useState(false) | ||
const [isClamped, setIsClamped] = useState(false) | ||
const textContentRef = useRef<HTMLParagraphElement | null>(null) | ||
|
||
useEffect(() => { | ||
if (textContentRef.current) { | ||
const { scrollHeight, clientHeight } = textContentRef.current | ||
setIsClamped(scrollHeight > clientHeight) | ||
} | ||
/** | ||
* Including [text, isExpanded] in dependencies even though they're not directly used | ||
* because: | ||
* - text changes affect scrollHeight and require recalculation | ||
* - isExpanded affects layout/styling, requiring fresh calculation | ||
*/ | ||
}, [text, isExpanded]) | ||
|
||
const toggleExpanded = () => { | ||
setIsExpanded((previousIsExpanded) => !previousIsExpanded) | ||
} | ||
|
||
return ( | ||
<div data-testid={testId} ref={ref} data-fs-review-card {...otherProps}> | ||
<div data-fs-review-card-header> | ||
<Rating value={rating} /> | ||
{author && ( | ||
<ReviewCardAuthor | ||
data-fs-review-card-author="desktop" | ||
author={author} | ||
isVerified={Boolean(isVerified)} | ||
verifiedText={verifiedText} | ||
/> | ||
)} | ||
{date && ( | ||
<ReviewCardDate | ||
data-fs-review-card-date="mobile" | ||
date={date} | ||
todayLabel={todayLabel} | ||
/> | ||
)} | ||
</div> | ||
<div data-fs-review-card-text> | ||
<div data-fs-review-card-text-header> | ||
<h3 data-fs-review-card-text-title>{title}</h3> | ||
|
||
{date && ( | ||
<ReviewCardDate | ||
data-fs-review-card-date="desktop" | ||
date={date} | ||
todayLabel={todayLabel} | ||
/> | ||
)} | ||
</div> | ||
<p | ||
ref={textContentRef} | ||
data-fs-review-card-text-content={ | ||
isExpanded ? 'expanded' : 'collapsed' | ||
} | ||
> | ||
{text} | ||
</p> | ||
{(isClamped || isExpanded) && ( | ||
<Link | ||
data-fs-review-card-text-read-more | ||
onClick={toggleExpanded} | ||
size="small" | ||
as="button" | ||
> | ||
{isExpanded ? readLessText : readMoreText} | ||
</Link> | ||
)} | ||
</div> | ||
{author && ( | ||
<ReviewCardAuthor | ||
data-fs-review-card-author="mobile" | ||
author={author} | ||
isVerified={Boolean(isVerified)} | ||
/> | ||
)} | ||
</div> | ||
) | ||
} | ||
) | ||
|
||
export default ReviewCard |
44 changes: 44 additions & 0 deletions
44
packages/components/src/molecules/ReviewCard/ReviewCardAuthor.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
import React, { forwardRef } from 'react' | ||
import type { HTMLAttributes } from 'react' | ||
import Tooltip from '../Tooltip' | ||
import Icon from '../../atoms/Icon' | ||
|
||
export interface ReviewCardAuthorProps extends HTMLAttributes<HTMLDivElement> { | ||
/** | ||
* The author of the review. | ||
*/ | ||
author: string | ||
/** | ||
* Whether the author is verified or not. Defaults to false. | ||
*/ | ||
isVerified: boolean | ||
/** | ||
* Text to be displayed in the tooltip when hovering over the verified icon. Defaults to 'Verified User'. | ||
*/ | ||
verifiedText?: string | ||
} | ||
|
||
const ReviewCardAuthor = forwardRef<HTMLDivElement, ReviewCardAuthorProps>( | ||
function ReviewCardAuthor( | ||
{ author, isVerified, verifiedText = 'Verified User', ...otherProps }, | ||
ref | ||
) { | ||
return ( | ||
<div data-fs-review-card-author {...otherProps} ref={ref}> | ||
<span data-fs-review-card-author-name>{author}</span> | ||
{isVerified && ( | ||
<Tooltip content={verifiedText} placement="bottom-center"> | ||
<Icon | ||
data-fs-review-card-author-verified | ||
name="CircleWavyCheck" | ||
width={20} | ||
height={20} | ||
/> | ||
</Tooltip> | ||
)} | ||
</div> | ||
) | ||
} | ||
) | ||
|
||
export default ReviewCardAuthor |
36 changes: 36 additions & 0 deletions
36
packages/components/src/molecules/ReviewCard/ReviewCardDate.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
import React, { forwardRef } from 'react' | ||
import type { HTMLAttributes } from 'react' | ||
import { formatDate } from '../../utils/date' | ||
|
||
export interface ReviewCardDateProps extends HTMLAttributes<HTMLDivElement> { | ||
/** | ||
* The date of the review. | ||
*/ | ||
date: Date | ||
/** | ||
* Optional locale to format the date. Defaults to 'en-US'. | ||
*/ | ||
locale?: string | ||
/** | ||
* The string label to display when the date is today. | ||
*/ | ||
todayLabel: string | ||
} | ||
|
||
const ReviewCardDate = forwardRef<HTMLDivElement, ReviewCardDateProps>( | ||
function ReviewCardDate( | ||
{ date, locale = 'en-US', todayLabel = 'Today', ...otherProps }, | ||
ref | ||
) { | ||
const todayString = formatDate(new Date(), locale) | ||
const dateString = formatDate(date, locale) | ||
|
||
return ( | ||
<span data-fs-review-card-date {...otherProps} ref={ref}> | ||
{dateString === todayString ? todayLabel : dateString} | ||
</span> | ||
) | ||
} | ||
) | ||
|
||
export default ReviewCardDate |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export { default } from './ReviewCard' | ||
export { ReviewCardProps } from './ReviewCard' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
/** | ||
* Formats date into a localized string. | ||
* @param date - The Date object to format | ||
* @param locale - The locale identifier (e.g., 'en-US', 'fr-FR'). Defaults to 'en-US' | ||
* @returns A string in the format "MMM D, YYYY" for en-US (e.g., "Jan 15, 2024") | ||
* Format will vary based on the provided locale | ||
*/ | ||
export function formatDate(date: Date, locale = 'en-US'): string { | ||
const options: Intl.DateTimeFormatOptions = { | ||
year: 'numeric', | ||
month: 'short', | ||
day: 'numeric', | ||
} | ||
|
||
return date.toLocaleDateString(locale, options) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.