Menu

Change language

GitHub repo

Testimonial — Social Cards

blockmarketing

Masonry of social-post style endorsements with handle, avatar, timestamp, body copy, and engagement counts, formatted as posts rather than polished marketing quotes.

Installation

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

Variants

Loading interactive previews…

Schema

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

SocialPost
interface SocialPost {
  name: string
  handle: string
  initials: string
  when: string
  body: string
  replies: number
  reposts: number
  likes: number
}

npm dependencies

Files installed (2)

  • src/app/components/blocks/testimonial-social-cards/testimonial-social-cards.component.ts5.9 kB
    // Masonry of social-post style endorsements with handle, avatar, timestamp, body copy,
    // and engagement counts — written as posts, not polished quotes. Port of the Vue/React block 1:1.
    import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
    import { Heart, LucideAngularModule, MessageCircle, Repeat2 } 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'
    
    export interface SocialPost {
      name: string
      handle: string
      initials: string
      when: string
      body: string
      replies: number
      reposts: number
      likes: number
    }
    
    export const socialPosts: SocialPost[] = [
      {
        name: 'Erin Walsh',
        handle: '@erinwalsh',
        initials: 'EW',
        when: '4h',
        body: 'closed the month in four days. four. i have been doing this for eleven years and that number has never started with a four.',
        replies: 18,
        reposts: 42,
        likes: 310,
      },
      {
        name: 'Marcus Ellery',
        handle: '@mellery',
        initials: 'ME',
        when: '1d',
        body: 'the thing that sold me was the diff. every metric change opens a PR. i can finally review what finance is about to publish instead of finding out in the board deck.',
        replies: 7,
        reposts: 91,
        likes: 604,
      },
      {
        name: 'Anna Reyes',
        handle: '@annareyes',
        initials: 'AR',
        when: '2d',
        body: 'migration took longer than the marketing site says (three weeks, not one) but nothing broke and we did not copy a single row out of the warehouse.',
        replies: 24,
        reposts: 33,
        likes: 188,
      },
      {
        name: 'Tom Fairbanks',
        handle: '@tfairbanks',
        initials: 'TF',
        when: '3d',
        body: 'row level scoping wired to okta groups. someone changes teams, their dashboards change with them. this used to be a quarterly cleanup ticket.',
        replies: 11,
        reposts: 57,
        likes: 402,
      },
      {
        name: 'Priya Raman',
        handle: '@praman',
        initials: 'PR',
        when: '5d',
        body: 'forecast review used to open with twenty minutes of reconciling two decks. now it opens with the forecast. small change, enormous difference.',
        replies: 5,
        reposts: 28,
        likes: 233,
      },
      {
        name: 'Sophie Lindqvist',
        handle: '@slindqvist',
        initials: 'SL',
        when: '1w',
        body: 'handed the auditors an exported change log instead of a shared folder and they were visibly confused about what to do with their afternoon.',
        replies: 31,
        reposts: 120,
        likes: 870,
      },
    ]
    
    @Component({
      changeDetection: ChangeDetectionStrategy.Eager,
      selector: 'ui-testimonial-social-cards, [ui-testimonial-social-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,
      ],
      template: `
        <section data-slot="testimonial-social-cards" [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">In the wild</span>
              <h2 class="mt-4 text-3xl font-semibold tracking-tight sm:text-4xl">Posted without being asked</h2>
              <p class="text-muted-foreground mt-3 text-lg">
                Unedited, including the one about the migration taking three weeks.
              </p>
            </div>
    
            <!-- CSS columns give a masonry flow with no JS layout pass; cards use
                 break-inside to avoid splitting across a column boundary. -->
            <div class="mt-10 gap-4 sm:columns-2 lg:columns-3">
              @for (post of posts; track post.handle) {
                <div ui-card class="mb-4 break-inside-avoid">
                  <div ui-card-content class="p-5">
                    <div class="flex items-center gap-3">
                      <ui-avatar class="size-9">
                        <ui-avatar-fallback class="text-xs">{{ post.initials }}</ui-avatar-fallback>
                      </ui-avatar>
                      <div class="min-w-0">
                        <p class="truncate text-sm font-medium">{{ post.name }}</p>
                        <p class="text-muted-foreground truncate text-xs">{{ post.handle }} · {{ post.when }}</p>
                      </div>
                    </div>
    
                    <p class="mt-3 text-sm leading-relaxed">{{ post.body }}</p>
    
                    <div class="text-muted-foreground mt-4 flex items-center gap-5 text-xs">
                      <button ui-button variant="ghost" size="sm" class="h-auto gap-1.5 px-1.5 py-1 text-xs font-normal">
                        <lucide-icon [img]="MessageCircle" class="size-3.5" aria-hidden="true" />
                        {{ post.replies }}
                      </button>
                      <button ui-button variant="ghost" size="sm" class="h-auto gap-1.5 px-1.5 py-1 text-xs font-normal">
                        <lucide-icon [img]="Repeat2" class="size-3.5" aria-hidden="true" />
                        {{ post.reposts }}
                      </button>
                      <button ui-button variant="ghost" size="sm" class="h-auto gap-1.5 px-1.5 py-1 text-xs font-normal">
                        <lucide-icon [img]="Heart" class="size-3.5" aria-hidden="true" />
                        {{ post.likes }}
                      </button>
                    </div>
                  </div>
                </div>
              }
            </div>
          </div>
        </section>
      `,
    })
    export class UiTestimonialSocialCardsComponent {
      protected readonly Heart = Heart
      protected readonly MessageCircle = MessageCircle
      protected readonly Repeat2 = Repeat2
    
      protected readonly posts = socialPosts
    
      @Input('class') className?: string
    
      get rootClass(): string {
        return cn('bg-background', this.className)
      }
    }
    
  • src/app/components/blocks/testimonial-social-cards/index.ts0.1 kB

Raw manifest:https://uipkge.dev/r/angular/testimonial-social-cards.json