Ratings — Star Summary
blockmarketingAggregate rating summary pairing a large score and partial-star row with a per-star distribution and the review count each bar represents.
Installation
$pnpm dlx uipkge-ng@latest add ratings-star-summary$npx uipkge-ng@latest add ratings-star-summary$yarn dlx uipkge-ng@latest add ratings-star-summary$bunx uipkge-ng@latest add ratings-star-summaryManifest:
https://uipkge.dev/r/angular/ratings-star-summary.jsonInstalls to:src/app/components/blocks/ratings-star-summary/Variants
Loading interactive previews…
Loading interactive previews…
Schema
Type aliases exported from this item's source. Use these to shape the data you pass in.
DistributionRowinterface DistributionRow {
stars: number
count: number
}Files installed (2)
src/app/components/blocks/ratings-star-summary/ratings-star-summary.component.ts4.8 kB
// Aggregate rating summary pairing a large score and partial-star row with a per-star // distribution and the review count each bar represents. Port of the React block 1:1. import { ChangeDetectionStrategy, Component, Input } from '@angular/core' import { LucideAngularModule, Star } from 'lucide-angular' import { cn } from '@/lib/utils' import { UiBadgeComponent } from '@/ui/badge/badge.component' import { UiButtonComponent } from '@/ui/button/button.component' import { UiCardComponent, UiCardContentComponent } from '@/ui/card/card.component' import { UiProgressComponent } from '@/ui/progress/progress.component' import { UiSeparatorComponent } from '@/ui/separator/separator.component' interface DistributionRow { stars: number count: number } const score = 4.7 const total = 2117 const distribution: DistributionRow[] = [ { stars: 5, count: 1587 }, { stars: 4, count: 381 }, { stars: 3, count: 97 }, { stars: 2, count: 31 }, { stars: 1, count: 21 }, ] const STAR_SLOTS = [1, 2, 3, 4, 5] /** Fill percentage for one slot, so 4.7 draws a real partial star. */ function fillFor(slot: number): string { return `${Math.min(Math.max(score - (slot - 1), 0), 1) * 100}%` } @Component({ changeDetection: ChangeDetectionStrategy.Eager, selector: 'ui-ratings-star-summary, [ui-ratings-star-summary]', standalone: true, // The block renders its own root <section>: keep this wrapper out of the layout. host: { '[attr.class]': '"contents"' }, imports: [ LucideAngularModule, UiBadgeComponent, UiButtonComponent, UiCardComponent, UiCardContentComponent, UiProgressComponent, UiSeparatorComponent, ], template: ` <section data-slot="ratings-star-summary" [class]="rootClass"> <div class="mx-auto max-w-4xl px-6 py-20 lg:py-28"> <span ui-badge variant="secondary">Reviews</span> <h2 class="mt-4 text-3xl font-semibold tracking-tight sm:text-4xl">What {{ total }} people say</h2> <div ui-card class="mt-8"> <div ui-card-content class="grid gap-8 p-6 sm:grid-cols-[auto_1fr] sm:gap-12 sm:p-8"> <div class="text-center sm:text-left"> <p class="font-display text-5xl font-bold tracking-tight">{{ score }}</p> <div class="mt-2 flex items-center justify-center gap-0.5 sm:justify-start" role="img" [attr.aria-label]="score + ' out of 5'" > @for (slot of starSlots; track slot) { <span class="relative inline-flex"> <lucide-icon [img]="Star" class="text-muted-foreground/30 size-4" aria-hidden="true" /> <span class="absolute inset-y-0 left-0 overflow-hidden" [style.width]="fillFor(slot)" aria-hidden="true" > <lucide-icon [img]="Star" class="fill-primary text-primary size-4" /> </span> </span> } </div> <p class="text-muted-foreground mt-2 text-xs">{{ total }} verified reviews</p> </div> <ui-separator orientation="vertical" class="hidden h-auto sm:block" /> <dl class="space-y-2.5"> @for (row of distribution; track row.stars) { <div class="flex items-center gap-4"> <dt class="text-muted-foreground w-12 shrink-0 font-mono text-xs">{{ row.stars }} star</dt> <dd class="flex min-w-0 grow items-center gap-4"> <div ui-progress [value]="barShare(row)" class="h-1.5" [aria-label]="row.stars + ' star reviews'" ></div> <span class="text-muted-foreground w-12 shrink-0 text-right font-mono text-xs"> {{ row.count }} </span> </dd> </div> } </dl> </div> </div> <div class="mt-6 flex flex-wrap items-center gap-4"> <button ui-button variant="outline">Read the one-star reviews</button> <p class="text-muted-foreground text-sm">They are the useful ones, and we have not hidden them.</p> </div> </div> </section> `, }) export class UiRatingsStarSummaryComponent { protected readonly Star = Star protected readonly score = score.toFixed(1) protected readonly total = total.toLocaleString() protected readonly distribution = distribution protected readonly starSlots = STAR_SLOTS @Input('class') className?: string get rootClass(): string { return cn('bg-background', this.className) } fillFor(slot: number): string { return fillFor(slot) } barShare(row: DistributionRow): number { return Math.round((row.count / total) * 100) } }src/app/components/blocks/ratings-star-summary/index.ts0.1 kB
export { UiRatingsStarSummaryComponent } from './ratings-star-summary.component'
Raw manifest:https://uipkge.dev/r/angular/ratings-star-summary.json