Menu

Change language

GitHub repo

Ratings & Review Badges

blockmarketing

Third-party proof strip: four review-platform cards with star rating, score out of five, review count, and a category award line, over a rating-distribution bar and a verified-reviews footnote.

Installation

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

Variants

Loading interactive previews…

Schema

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

Platform
interface Platform {
  name: string
  score: number
  reviews: number
  award: string
}
DistributionRow
interface DistributionRow {
  stars: number
  share: number
}

Files installed (2)

  • src/app/components/blocks/ratings-review-badges/ratings-review-badges.component.ts5.9 kB
    // Third-party proof strip: four review-platform cards with star rating, score out of
    // five, review count, and a category award line, over a rating-distribution bar and a
    // verified-reviews footnote. 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 Platform {
      name: string
      score: number
      reviews: number
      award: string
    }
    
    interface DistributionRow {
      stars: number
      share: number
    }
    
    const platforms: Platform[] = [
      { name: 'G2', score: 4.8, reviews: 412, award: 'Leader — Analytics Platforms, Winter 2026' },
      { name: 'Capterra', score: 4.7, reviews: 268, award: 'Best Value — Business Intelligence' },
      { name: 'Product Hunt', score: 4.9, reviews: 1340, award: '#1 Product of the Day' },
      { name: 'TrustRadius', score: 4.6, reviews: 97, award: 'Top Rated — Data Governance' },
    ]
    
    const distribution: DistributionRow[] = [
      { stars: 5, share: 78 },
      { stars: 4, share: 16 },
      { stars: 3, share: 4 },
      { stars: 2, share: 1 },
      { stars: 1, share: 1 },
    ]
    
    const STAR_SLOTS = [1, 2, 3, 4, 5]
    
    /** Fill percentage for one star slot, so a 4.6 renders a real partial star. */
    function fillFor(score: number, slot: number): string {
      return `${Math.min(Math.max(score - (slot - 1), 0), 1) * 100}%`
    }
    
    @Component({
      changeDetection: ChangeDetectionStrategy.Eager,
      selector: 'ui-ratings-review-badges, [ui-ratings-review-badges]',
      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-review-badges" [class]="rootClass">
          <div class="mx-auto max-w-6xl px-6 py-20 lg:py-28">
            <div class="max-w-2xl">
              <span ui-badge variant="secondary">Reviews</span>
              <h2 class="mt-4 text-3xl font-semibold tracking-tight sm:text-4xl">Rated by the people who run it daily</h2>
              <p class="text-muted-foreground mt-3 text-lg">
                Every score below is pulled from a public review platform. None of them are ours to edit.
              </p>
            </div>
    
            <div class="mt-10 grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
              @for (platform of platforms; track platform.name) {
                <div ui-card>
                  <div ui-card-content class="p-5">
                    <div class="flex items-baseline justify-between gap-3">
                      <p class="text-sm font-semibold">{{ platform.name }}</p>
                      <p class="text-muted-foreground font-mono text-xs">{{ platform.reviews }} reviews</p>
                    </div>
    
                    <div class="mt-3 flex items-center gap-2">
                      <div class="flex items-center gap-0.5" role="img" [attr.aria-label]="platform.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" />
                            <!-- Dynamic clip width is the only way to draw a partial star. -->
                            <span
                              class="absolute inset-y-0 left-0 overflow-hidden"
                              [style.width]="fillFor(platform.score, slot)"
                              aria-hidden="true"
                            >
                              <lucide-icon [img]="Star" class="fill-primary text-primary size-4" />
                            </span>
                          </span>
                        }
                      </div>
                      <span class="text-sm font-medium">{{ platform.score.toFixed(1) }}</span>
                    </div>
    
                    <p class="text-muted-foreground mt-4 text-xs leading-relaxed">{{ platform.award }}</p>
                  </div>
                </div>
              }
            </div>
    
            <ui-separator class="my-10" />
    
            <div class="grid gap-10 lg:grid-cols-[1fr_1.4fr] lg:items-center">
              <div>
                <p class="font-display text-5xl font-bold tracking-tight">4.8</p>
                <p class="text-muted-foreground mt-2 text-sm">
                  Weighted average across 2,117 verified reviews on four platforms.
                </p>
                <button ui-button variant="outline" class="mt-5">Read the reviews</button>
              </div>
    
              <dl class="space-y-3">
                @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]="row.share" class="h-1.5" [aria-label]="row.stars + ' star reviews'"></div>
                      <span class="text-muted-foreground w-10 shrink-0 text-right font-mono text-xs">{{ row.share }}%</span>
                    </dd>
                  </div>
                }
              </dl>
            </div>
          </div>
        </section>
      `,
    })
    export class UiRatingsReviewBadgesComponent {
      protected readonly Star = Star
      protected readonly platforms = platforms
      protected readonly distribution = distribution
      protected readonly starSlots = STAR_SLOTS
    
      @Input('class') className?: string
    
      get rootClass(): string {
        return cn('bg-background', this.className)
      }
    
      fillFor(score: number, slot: number): string {
        return fillFor(score, slot)
      }
    }
    
  • src/app/components/blocks/ratings-review-badges/index.ts0.1 kB

Raw manifest:https://uipkge.dev/r/angular/ratings-review-badges.json