Menu

Change language

GitHub repo

Case Studies — Metric Cards

blockmarketing

Three-up customer results grid where each card leads with the headline metric, then the customer logo wordmark, a one-line outcome, the pull quote with attribution, and a link to the full story.

Installation

$npx uipkge-ng@latest add case-study-metric-cards
Manifest:https://uipkge.dev/r/angular/case-study-metric-cards.jsonInstalls to:src/app/components/blocks/case-study-metric-cards/

Variants

Loading interactive previews…

Schema

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

CaseStudyMetricCardStudy
interface CaseStudyMetricCardStudy {
  metric: string
  metricLabel: string
  company: string
  industry: string
  outcome: string
  quote: string
  author: { name: string; role: string; initials: string }
}

Files installed (2)

  • src/app/components/blocks/case-study-metric-cards/case-study-metric-cards.component.ts5.9 kB
    // Three-up customer results grid: each card leads with the headline metric, then the
    // customer wordmark, a one-line outcome, the pull quote with attribution, and a link to
    // the full story. Port of the React block 1:1.
    import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
    import { ArrowUpRight, LucideAngularModule } from 'lucide-angular'
    import { cn } from '@/lib/utils'
    import { UiAvatarComponent, UiAvatarFallbackComponent } from '@/ui/avatar/avatar.component'
    import { UiBadgeComponent } from '@/ui/badge/badge.component'
    import { UiButtonComponent } from '@/ui/button/button.component'
    import { UiCardComponent, UiCardContentComponent } from '@/ui/card/card.component'
    import { UiSeparatorComponent } from '@/ui/separator/separator.component'
    
    export interface CaseStudyMetricCardStudy {
      metric: string
      metricLabel: string
      company: string
      industry: string
      outcome: string
      quote: string
      author: { name: string; role: string; initials: string }
    }
    
    const defaultStudies: CaseStudyMetricCardStudy[] = [
      {
        metric: '5 days',
        metricLabel: 'off the monthly close',
        company: 'Northwind Logistics',
        industry: 'Freight forwarding · 1,200 staff',
        outcome: 'Replaced eleven reconciliation spreadsheets with one certified revenue model.',
        quote: 'We stopped arguing about whose number was right and started arguing about what to do next.',
        author: { name: 'Erin Walsh', role: 'VP Finance', initials: 'EW' },
      },
      {
        metric: '71%',
        metricLabel: 'fewer ad-hoc data requests',
        company: 'Halden Health',
        industry: 'Provider network · 40 clinics',
        outcome: 'Clinic leads self-serve their own quality metrics without filing a ticket.',
        quote: 'The analytics team went from a request queue to actually building things again.',
        author: { name: 'Daniel Brooks', role: 'Head of Analytics', initials: 'DB' },
      },
      {
        metric: '2.4pp',
        metricLabel: 'tighter forecast accuracy',
        company: 'Verity Retail Group',
        industry: 'Omnichannel retail · 310 stores',
        outcome: 'Sales and finance now forecast off the same pipeline definition, snapshotted weekly.',
        quote: 'Forecast review stopped opening with twenty minutes of reconciling two decks.',
        author: { name: 'Priya Raman', role: 'RevOps Director', initials: 'PR' },
      },
    ]
    
    @Component({
      changeDetection: ChangeDetectionStrategy.Eager,
      selector: 'ui-case-study-metric-cards, [ui-case-study-metric-cards]',
      standalone: true,
      // React renders the root <section> itself: the host stays out of layout and `class` goes to the root.
      host: { '[attr.class]': '"contents"' },
      imports: [
        LucideAngularModule,
        UiAvatarComponent,
        UiAvatarFallbackComponent,
        UiBadgeComponent,
        UiButtonComponent,
        UiCardComponent,
        UiCardContentComponent,
        UiSeparatorComponent,
      ],
      template: `
        <section data-slot="case-study-metric-cards" [class]="rootClass">
          <div class="mx-auto max-w-6xl px-6 py-20 lg:py-28">
            <div class="flex flex-wrap items-end justify-between gap-6">
              <div class="max-w-2xl">
                <span ui-badge variant="secondary">Customer results</span>
                <h2 class="mt-4 text-3xl font-semibold tracking-tight sm:text-4xl">What changed after the rollout</h2>
                <p class="text-muted-foreground mt-3 text-lg">
                  Three teams, measured against their own numbers from the quarter before.
                </p>
              </div>
              <button ui-button variant="outline">All case studies</button>
            </div>
    
            <div class="mt-10 grid gap-4 md:grid-cols-3">
              @for (study of studies; track study.company) {
                <div ui-card class="group">
                  <div ui-card-content class="flex h-full flex-col p-6">
                    <!-- Metric leads: it is the only part that survives a skim. -->
                    <p class="font-display text-4xl font-bold tracking-tight">{{ study.metric }}</p>
                    <p class="text-muted-foreground mt-1 text-sm">{{ study.metricLabel }}</p>
    
                    <ui-separator class="my-5" />
    
                    <p class="text-sm font-semibold">{{ study.company }}</p>
                    <p class="text-muted-foreground mt-0.5 text-xs">{{ study.industry }}</p>
                    <p class="mt-4 text-sm leading-relaxed">{{ study.outcome }}</p>
    
                    <blockquote class="border-border text-muted-foreground mt-5 border-l-2 pl-4 text-sm italic">
                      &ldquo;{{ study.quote }}&rdquo;
                    </blockquote>
    
                    <div class="mt-auto flex items-center gap-3 pt-6">
                      <ui-avatar class="size-8">
                        <ui-avatar-fallback class="text-xs">{{ study.author.initials }}</ui-avatar-fallback>
                      </ui-avatar>
                      <div class="min-w-0">
                        <p class="truncate text-sm font-medium">{{ study.author.name }}</p>
                        <p class="text-muted-foreground truncate text-xs">{{ study.author.role }}</p>
                      </div>
                      <button
                        ui-button
                        variant="ghost"
                        size="icon"
                        class="ml-auto"
                        [attr.aria-label]="'Read the ' + study.company + ' case study'"
                      >
                        <lucide-icon
                          [img]="ArrowUpRight"
                          class="size-4 transition-transform group-hover:translate-x-0.5 group-hover:-translate-y-0.5"
                          aria-hidden="true"
                        />
                      </button>
                    </div>
                  </div>
                </div>
              }
            </div>
          </div>
        </section>
      `,
    })
    export class UiCaseStudyMetricCardsComponent {
      protected readonly ArrowUpRight = ArrowUpRight
    
      @Input() studies: CaseStudyMetricCardStudy[] = defaultStudies.map((s) => ({ ...s, author: { ...s.author } }))
      @Input('class') className?: string
    
      get rootClass(): string {
        return cn('bg-background', this.className)
      }
    }
    
  • src/app/components/blocks/case-study-metric-cards/index.ts0.1 kB

Raw manifest:https://uipkge.dev/r/angular/case-study-metric-cards.json