Skip to content

Commit

Permalink
feat: add review card (#2645)
Browse files Browse the repository at this point in the history
## 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)


![image](https://github.com/user-attachments/assets/3cbab6a6-7d9a-424b-96e5-91605bd26a17)


![image](https://github.com/user-attachments/assets/3c0b4c03-c19f-415e-a277-ad35c148f1eb)

---------

Co-authored-by: Fanny Chien <fanny.chien@vtex.com>
Co-authored-by: Pedro Soares <32311264+pedromtec@users.noreply.github.com>
  • Loading branch information
3 people authored Feb 20, 2025
1 parent 7379ea7 commit 5b1d946
Show file tree
Hide file tree
Showing 11 changed files with 593 additions and 1 deletion.
2 changes: 2 additions & 0 deletions packages/components/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,8 @@ export { default as RegionBar } from './molecules/RegionBar'
export type { RegionBarProps } from './molecules/RegionBar'
export { default as Tooltip } from './molecules/Tooltip'
export type { TooltipProps } from './molecules/Tooltip'
export { default as ReviewCard } from './molecules/ReviewCard'
export type { ReviewCardProps } from './molecules/ReviewCard'

export { default as SearchProvider } from './molecules/SearchProvider'
export type { SearchProviderContextValue } from './molecules/SearchProvider'
Expand Down
143 changes: 143 additions & 0 deletions packages/components/src/molecules/ReviewCard/ReviewCard.tsx
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 packages/components/src/molecules/ReviewCard/ReviewCardAuthor.tsx
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 packages/components/src/molecules/ReviewCard/ReviewCardDate.tsx
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
2 changes: 2 additions & 0 deletions packages/components/src/molecules/ReviewCard/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { default } from './ReviewCard'
export { ReviewCardProps } from './ReviewCard'
16 changes: 16 additions & 0 deletions packages/components/src/utils/date.ts
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)
}
3 changes: 2 additions & 1 deletion packages/storybook/.storybook/main.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { join, dirname } from 'path'
import { dirname, join } from 'path'

/**
* This function is used to resolve the absolute path of a package.
Expand All @@ -24,5 +24,6 @@ const config = {
name: getAbsolutePath('@storybook/nextjs'),
options: {},
},
staticDirs: ['../public'],
}
export default config
Loading

0 comments on commit 5b1d946

Please sign in to comment.