Skip to content

Commit 4ab3436

Browse files
authored
Merge pull request #102 from makeswift/andrew/ratings-style
feat: add total reviews count to ratings
2 parents a1b9666 + 8fa2800 commit 4ab3436

File tree

8 files changed

+116
-77
lines changed

8 files changed

+116
-77
lines changed

src/components/rating/primitives.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,7 @@ export {
1313
RatingValue as Value,
1414
type RatingValueProps as ValueProps,
1515
} from '@/components/rating/primitives/rating-value';
16+
export {
17+
RatingTotal as Total,
18+
type RatingTotalProps as TotalProps,
19+
} from '@/components/rating/primitives/rating-total';

src/components/rating/primitives/rating-root.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ interface RatingContext {
1212
adjustedRating: number;
1313
stars: StarType[];
1414
showRating: boolean;
15+
showTotalReviews: boolean;
16+
totalReviews: number;
1517
getNextStarIndex: () => number;
1618
resetStarIndex: () => void;
1719
}
@@ -21,6 +23,8 @@ const RatingContext = createContext<RatingContext | undefined>(undefined);
2123
export interface RatingRootProps extends ComponentProps<'div'> {
2224
rating: number;
2325
showRating?: boolean;
26+
showTotalReviews?: boolean;
27+
totalReviews?: number;
2428
children: ReactNode;
2529
}
2630

@@ -29,6 +33,8 @@ export function RatingRoot({
2933
className,
3034
rating,
3135
showRating = true,
36+
showTotalReviews = true,
37+
totalReviews = 0,
3238
...props
3339
}: RatingRootProps) {
3440
const starIndexRef = useRef(0);
@@ -60,14 +66,16 @@ export function RatingRoot({
6066
adjustedRating,
6167
stars,
6268
showRating,
69+
showTotalReviews,
70+
totalReviews,
6371
getNextStarIndex,
6472
resetStarIndex,
6573
};
66-
}, [rating, showRating, getNextStarIndex, resetStarIndex]);
74+
}, [rating, showRating, showTotalReviews, totalReviews, getNextStarIndex, resetStarIndex]);
6775

6876
return (
6977
<RatingContext.Provider value={contextValues}>
70-
<div className={cn('flex items-center', className)} {...props}>
78+
<div className={cn('flex items-center', className)} data-slot="rating-root" {...props}>
7179
{children}
7280
</div>
7381
</RatingContext.Provider>

src/components/rating/primitives/rating-star.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ export function RatingStar({ className, ...props }: RatingStarProps) {
5252
return (
5353
<svg
5454
className={cn('inline-block text-[var(--rating-icon,hsl(var(--foreground)))]', className)}
55+
data-slot="rating-star"
5556
fill="none"
5657
height={20}
5758
viewBox="0 0 20 20"

src/components/rating/primitives/rating-stars.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ export function RatingStars() {
1010
return (
1111
<>
1212
{[0, 1, 2, 3, 4].map((i) => (
13-
<RatingStar key={i} />
13+
<RatingStar data-slot="rating-star" key={i} />
1414
))}
1515
</>
1616
);
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
'use client';
2+
3+
import type { ComponentProps } from 'react';
4+
5+
import { useRating } from '@/components/rating';
6+
import { cn } from '@/lib';
7+
8+
export type RatingTotalProps = ComponentProps<'span'>;
9+
10+
export function RatingTotal({ className, children, ...props }: RatingTotalProps) {
11+
const { totalReviews, showTotalReviews } = useRating();
12+
13+
if (!showTotalReviews) return null;
14+
15+
return (
16+
<span
17+
className={cn(
18+
'ml-2 border-l border-contrast-200 pl-2 font-normal text-[var(--rating-text,hsl(var(--contrast-500)))]',
19+
className,
20+
)}
21+
data-slot="rating-total"
22+
{...props}
23+
>
24+
{totalReviews.toLocaleString()} {totalReviews === 1 ? 'review' : 'reviews'}
25+
</span>
26+
);
27+
}

src/components/rating/primitives/rating-value.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,22 @@ import { cn } from '@/lib';
77

88
export type RatingValueProps = ComponentProps<'span'>;
99

10-
export function RatingValue({ className, ...props }: RatingValueProps) {
10+
export function RatingValue({ className, children, ...props }: RatingValueProps) {
1111
const { adjustedRating, showRating } = useRating();
1212

1313
if (!showRating) return null;
1414

1515
return (
1616
<span
1717
className={cn(
18-
'ml-1.5 flex h-6 min-w-6 shrink-0 items-center justify-center rounded-full border border-[var(--rating-border,hsl(var(--contrast-100)))] px-1 text-xs font-medium text-[var(--rating-text,hsl(var(--contrast-400)))]',
18+
'ml-2 flex text-xs font-bold leading-normal text-[var(--rating-text,hsl(var(--foreground)))]',
1919
className,
2020
)}
21+
data-slot="rating-value"
2122
{...props}
2223
>
2324
{adjustedRating % 1 !== 0 ? adjustedRating.toFixed(1) : adjustedRating}
25+
{children}
2426
</span>
2527
);
2628
}

src/components/rating/rating.tsx

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ import * as RatingPrimitive from '@/components/rating';
22

33
export interface RatingProps {
44
showRating?: boolean;
5+
showTotalReviews?: boolean;
56
rating: number;
7+
totalReviews?: number;
68
className?: string;
79
}
810

@@ -18,11 +20,25 @@ export interface RatingProps {
1820
* }
1921
* ```
2022
*/
21-
export function Rating({ showRating = true, rating, className }: Readonly<RatingProps>) {
23+
export function Rating({
24+
showRating = true,
25+
showTotalReviews = true,
26+
rating,
27+
totalReviews,
28+
className,
29+
}: RatingProps) {
2230
return (
23-
<RatingPrimitive.Root className={className} rating={rating} showRating={showRating}>
31+
<RatingPrimitive.Root
32+
className={className}
33+
rating={rating}
34+
showRating={showRating}
35+
showTotalReviews={showTotalReviews}
36+
totalReviews={totalReviews}
37+
>
2438
<RatingPrimitive.Stars />
25-
<RatingPrimitive.Value />
39+
<RatingPrimitive.Value>
40+
<RatingPrimitive.Total />
41+
</RatingPrimitive.Value>
2642
</RatingPrimitive.Root>
2743
);
2844
}
Lines changed: 50 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import type { Meta, StoryObj } from '@storybook/react-vite';
22

3-
import { Rating } from '@/components/rating';
3+
import * as RatingPrimitive from '@/components/rating';
4+
import { Rating, type RatingProps } from '@/components/rating';
45

5-
const meta = {
6+
const meta: Meta<typeof Rating> = {
67
title: 'Components/Rating',
78
component: Rating,
89
parameters: {
@@ -12,117 +13,97 @@ const meta = {
1213
argTypes: {
1314
rating: {
1415
control: { type: 'number', min: 0, max: 5, step: 0.1 },
15-
description: 'Rating value from 0 to 5',
16+
description: 'The rating value (0-5)',
17+
},
18+
totalReviews: {
19+
control: 'number',
20+
description: 'Total number of reviews to display',
1621
},
1722
showRating: {
1823
control: 'boolean',
19-
description: 'Show numeric rating badge',
20-
table: {
21-
defaultValue: { summary: 'true' },
22-
},
24+
description: 'Whether to show the numeric rating value',
25+
},
26+
showTotalReviews: {
27+
control: 'boolean',
28+
description: 'Whether to show the total reviews count',
2329
},
2430
className: {
2531
control: 'text',
26-
description: 'Additional CSS classes',
32+
description: 'Additional CSS classes for the root element',
2733
},
2834
},
29-
args: {
30-
rating: 4.5,
31-
showRating: true,
32-
},
33-
} satisfies Meta<typeof Rating>;
35+
};
3436

3537
export default meta;
36-
37-
type Story = StoryObj<typeof meta>;
38+
type Story = StoryObj<RatingProps>;
3839

3940
export const Default: Story = {
4041
args: {
4142
rating: 4.5,
43+
totalReviews: 128,
4244
},
4345
};
4446

45-
export const FiveStars: Story = {
47+
export const FullStars: Story = {
4648
args: {
4749
rating: 5,
50+
totalReviews: 1024,
4851
},
4952
};
5053

51-
export const FourStars: Story = {
54+
export const HalfStar: Story = {
5255
args: {
53-
rating: 4,
54-
},
55-
};
56-
57-
export const ThreeStars: Story = {
58-
args: {
59-
rating: 3,
60-
},
61-
};
62-
63-
export const TwoStars: Story = {
64-
args: {
65-
rating: 2,
66-
},
67-
};
68-
69-
export const OneStar: Story = {
70-
args: {
71-
rating: 1,
56+
rating: 3.5,
57+
totalReviews: 42,
7258
},
7359
};
7460

75-
export const ZeroStars: Story = {
61+
export const LowRating: Story = {
7662
args: {
77-
rating: 0,
63+
rating: 1.5,
64+
totalReviews: 7,
7865
},
7966
};
8067

81-
export const HalfStars: Story = {
68+
export const SingleReview: Story = {
8269
args: {
83-
rating: 3.5,
70+
rating: 5,
71+
totalReviews: 1,
8472
},
8573
};
8674

87-
export const WithoutBadge: Story = {
75+
export const StarsOnly: Story = {
8876
args: {
89-
rating: 4.5,
77+
rating: 4,
9078
showRating: false,
79+
showTotalReviews: false,
9180
},
9281
};
9382

94-
export const AllRatings: Story = {
83+
export const WithoutTotalReviews: Story = {
9584
args: {
9685
rating: 4.5,
86+
showTotalReviews: false,
9787
},
98-
render: () => (
99-
<div className="flex flex-col gap-4">
100-
<Rating rating={5} />
101-
<Rating rating={4.5} />
102-
<Rating rating={4} />
103-
<Rating rating={3.5} />
104-
<Rating rating={3} />
105-
<Rating rating={2.5} />
106-
<Rating rating={2} />
107-
<Rating rating={1.5} />
108-
<Rating rating={1} />
109-
<Rating rating={0.5} />
110-
<Rating rating={0} />
111-
</div>
112-
),
11388
};
11489

115-
export const WithoutBadges: Story = {
116-
args: {
117-
rating: 4.5,
118-
},
90+
/**
91+
* The Rating component is built using composable primitives that can be used
92+
* independently to create custom layouts. Here's the anatomy:
93+
*
94+
* - `Root` - Context provider and container
95+
* - `Stars` - Renders all 5 stars based on the rating
96+
* - `Star` - Individual star icon (empty, half, or full)
97+
* - `Value` - Numeric rating display
98+
* - `Total` - Review count display
99+
*/
100+
export const ComposableAnatomy: Story = {
119101
render: () => (
120-
<div className="flex flex-col gap-4">
121-
<Rating rating={5} showRating={false} />
122-
<Rating rating={4.5} showRating={false} />
123-
<Rating rating={4} showRating={false} />
124-
<Rating rating={3.5} showRating={false} />
125-
<Rating rating={3} showRating={false} />
126-
</div>
102+
<RatingPrimitive.Root rating={4.5} totalReviews={256}>
103+
<RatingPrimitive.Stars />
104+
<RatingPrimitive.Value>
105+
<RatingPrimitive.Total />
106+
</RatingPrimitive.Value>
107+
</RatingPrimitive.Root>
127108
),
128109
};

0 commit comments

Comments
 (0)