Menu

Change language

GitHub repo

Ratings — Star Summary

blockmarketing

Aggregate rating summary pairing a large score and partial-star row with a per-star distribution and the review count each bar represents.

Installation

$npx uipkge-ng@latest add ratings-star-summary
Manifest:https://uipkge.dev/r/angular/ratings-star-summary.jsonInstalls to:src/app/components/blocks/ratings-star-summary/

Variants

Loading interactive previews…

Schema

Type aliases exported from this item's source. Use these to shape the data you pass in.

DistributionRow
interface 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

Raw manifest:https://uipkge.dev/r/angular/ratings-star-summary.json