Case Studies — Filterable List
blockmarketingFilterable index of customer stories with industry chips above rows carrying the headline result, stack, and read time, plus an empty state.
Installation
$pnpm dlx uipkge-ng@latest add case-study-filterable-list$npx uipkge-ng@latest add case-study-filterable-list$yarn dlx uipkge-ng@latest add case-study-filterable-list$bunx uipkge-ng@latest add case-study-filterable-listManifest:
https://uipkge.dev/r/angular/case-study-filterable-list.jsonInstalls to:src/app/components/blocks/case-study-filterable-list/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.
CaseStudyListStudyinterface CaseStudyListStudy {
company: string
industry: string
result: string
stack: string
readTime: string
}npm dependencies
Files installed (2)
src/app/components/blocks/case-study-filterable-list/case-study-filterable-list.component.ts6.6 kB
// Filterable index of customer stories: industry chips narrow rows carrying the headline // result, stack, and read time, with an empty state covering no matches. Port of the React // block 1:1. import { ChangeDetectionStrategy, Component, Input, computed, signal } from '@angular/core' import { ArrowUpRight, LucideAngularModule } 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 { UiSeparatorComponent } from '@/ui/separator/separator.component' import { UiToggleGroupComponent, UiToggleGroupItemComponent } from '@/ui/toggle-group/toggle-group.component' export interface CaseStudyListStudy { company: string industry: string result: string stack: string readTime: string } const defaultStudies: CaseStudyListStudy[] = [ { company: 'Northwind Logistics', industry: 'Logistics', result: '5 days off the monthly close', stack: 'Snowflake · dbt · Okta', readTime: '8 min', }, { company: 'Halden Health', industry: 'Healthcare', result: '71% fewer ad-hoc data requests', stack: 'BigQuery · Airflow', readTime: '6 min', }, { company: 'Verity Retail Group', industry: 'Retail', result: '2.4pp tighter forecast band', stack: 'Redshift · dbt', readTime: '7 min', }, { company: 'Calder Manufacturing', industry: 'Manufacturing', result: '11 spreadsheets retired', stack: 'Postgres · Dagster', readTime: '5 min', }, { company: 'Ridgeway Freight', industry: 'Logistics', result: 'Per-shipment landed cost', stack: 'Snowflake · Fivetran', readTime: '9 min', }, { company: 'Ashford Clinics', industry: 'Healthcare', result: 'PHI-scoped self-serve reporting', stack: 'BigQuery · Entra ID', readTime: '6 min', }, ] const ALL = 'All' @Component({ changeDetection: ChangeDetectionStrategy.Eager, selector: 'ui-case-study-filterable-list, [ui-case-study-filterable-list]', 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, UiBadgeComponent, UiButtonComponent, UiCardComponent, UiCardContentComponent, UiSeparatorComponent, UiToggleGroupComponent, UiToggleGroupItemComponent, ], template: ` <section data-slot="case-study-filterable-list" [class]="rootClass"> <div class="mx-auto max-w-4xl px-6 py-20 lg:py-28"> <div class="flex flex-wrap items-end justify-between gap-6"> <div> <span ui-badge variant="secondary">Customer stories</span> <h2 class="mt-4 text-3xl font-semibold tracking-tight sm:text-4xl">Read one close to your own</h2> </div> <div ui-toggle-group type="single" variant="outline" size="sm" class="flex-nowrap overflow-x-auto" aria-label="Filter case studies by industry" [value]="active()" (valueChange)="onFilterChange($event)" > @for (industry of industries(); track industry) { <button ui-toggle-group-item [value]="industry">{{ industry }}</button> } </div> </div> <div ui-card class="mt-8"> <div ui-card-content class="p-2"> @if (visible().length === 0) { <div class="px-4 py-16 text-center"> <p class="text-sm font-medium">No stories in that industry yet.</p> <button ui-button variant="outline" size="sm" class="mt-4" (click)="showAll()">Show all</button> </div> } @else { <ul> @for (study of visible(); track study.company; let last = $last) { <li> <a href="#" class="hover:bg-muted focus-visible:ring-ring group flex items-center gap-4 rounded-lg p-4 transition-colors focus-visible:ring-2 focus-visible:outline-none" (click)="$event.preventDefault()" > <div class="min-w-0 flex-1"> <div class="flex flex-wrap items-center gap-2"> <p class="text-sm font-semibold">{{ study.company }}</p> <span ui-badge variant="outline">{{ study.industry }}</span> </div> <p class="mt-1 text-sm">{{ study.result }}</p> <p class="text-muted-foreground mt-1 font-mono text-xs">{{ study.stack }}</p> </div> <span class="text-muted-foreground shrink-0 text-xs">{{ study.readTime }}</span> <lucide-icon [img]="ArrowUpRight" class="text-muted-foreground size-4 shrink-0 transition-transform group-hover:translate-x-0.5 group-hover:-translate-y-0.5" aria-hidden="true" /> </a> </li> @if (!last) { <ui-separator /> } } </ul> } </div> </div> </div> </section> `, }) export class UiCaseStudyFilterableListComponent { protected readonly ArrowUpRight = ArrowUpRight protected readonly allLabel = ALL @Input() set initialStudies(v: CaseStudyListStudy[] | undefined) { this.studies.set((v ?? defaultStudies).map((s) => ({ ...s }))) } @Input() set defaultIndustry(v: string | undefined) { this.active.set(v ?? ALL) } @Input('class') className?: string readonly studies = signal<CaseStudyListStudy[]>(defaultStudies.map((s) => ({ ...s }))) readonly active = signal<string>(ALL) readonly industries = computed(() => [ALL, ...new Set(this.studies().map((s) => s.industry))]) readonly visible = computed(() => { const active = this.active() return active === ALL ? this.studies() : this.studies().filter((s) => s.industry === active) }) get rootClass(): string { return cn('bg-background', this.className) } // Single-select emits '' when the pressed chip is pressed again; fall back to ALL so the // list is never empty by accident. onFilterChange(value: string | string[]): void { const next = Array.isArray(value) ? (value[0] ?? '') : value this.active.set(next || ALL) } showAll(): void { this.active.set(ALL) } }src/app/components/blocks/case-study-filterable-list/index.ts0.1 kB
export { UiCaseStudyFilterableListComponent, type CaseStudyListStudy } from './case-study-filterable-list.component'
Raw manifest:https://uipkge.dev/r/angular/case-study-filterable-list.json