Menu

Change language

GitHub repo

Knowledge Base Hub

blockmedia

Self-serve help center and customer documentation hub with hero search, quick topic pills, category cards grid, trending articles, and community support banner.

Installation

$npx uipkge-ng@latest add knowledge-base-hub
Manifest:https://uipkge.dev/r/angular/knowledge-base-hub.jsonInstalls to:src/app/components/blocks/knowledge-base-hub/

Variants

Loading interactive previews…

Schema

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

Category
interface Category {
  id: string
  title: string
  description: string
  articlesCount: number
  icon: unknown
  featuredTopics: string[]
}
Article
interface Article {
  id: string
  title: string
  description: string
  category: string
  readTime: string
  viewCount: string
}

npm dependencies

Files installed (2)

  • src/app/components/blocks/knowledge-base-hub/knowledge-base-hub.component.ts17.4 kB
    // Self-serve help center and customer documentation hub with hero search, quick topic pills,
    // category cards grid, trending articles, and community support banner. Port of the React block 1:1.
    import { ChangeDetectionStrategy, Component, Input, computed, signal } from '@angular/core'
    import {
      ArrowRight,
      ArrowUpRight,
      Blocks,
      BookOpen,
      Clock,
      Code,
      CreditCard,
      Eye,
      HelpCircle,
      LucideAngularModule,
      MessageSquare,
      Rocket,
      Search,
      Shield,
      Users,
      X,
    } 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,
      UiCardDescriptionComponent,
      UiCardFooterComponent,
      UiCardHeaderComponent,
      UiCardTitleComponent,
    } from '@/ui/card/card.component'
    import { UiInputComponent } from '@/ui/input/input.component'
    
    interface Category {
      id: string
      title: string
      description: string
      articlesCount: number
      icon: unknown
      featuredTopics: string[]
    }
    
    interface Article {
      id: string
      title: string
      description: string
      category: string
      readTime: string
      viewCount: string
    }
    
    const defaultCategories: Category[] = [
      {
        id: 'getting-started',
        title: 'Getting Started',
        icon: Rocket,
        articlesCount: 14,
        description: 'Quick start guides, installation walkthroughs, and core architecture concepts.',
        featuredTopics: ['Quickstart Guide', 'Project Setup', 'First Deployment'],
      },
      {
        id: 'account-billing',
        title: 'Account & Billing',
        icon: CreditCard,
        articlesCount: 22,
        description: 'Subscription plans, payment methods, invoice management, and seat licensing.',
        featuredTopics: ['Upgrade Plan', 'Payment Methods', 'Invoices'],
      },
      {
        id: 'api-sdks',
        title: 'API & Developer SDKs',
        icon: Code,
        articlesCount: 35,
        description: 'REST & GraphQL endpoints, SDK reference libraries, auth tokens, and rate limits.',
        featuredTopics: ['Authentication', 'Rate Limits', 'SDK Libraries'],
      },
      {
        id: 'security-compliance',
        title: 'Security & Compliance',
        icon: Shield,
        articlesCount: 18,
        description: 'SOC 2 certification, SSO/SAML configuration, audit logs, and data encryption.',
        featuredTopics: ['SAML SSO', 'Audit Logs', 'Data Encryption'],
      },
      {
        id: 'integrations-webhooks',
        title: 'Integrations & Webhooks',
        icon: Blocks,
        articlesCount: 28,
        description: 'Connecting third-party apps, custom webhooks, payload schemas, and event retries.',
        featuredTopics: ['Webhooks Setup', 'Slack App', 'Retry Policies'],
      },
      {
        id: 'troubleshooting-faq',
        title: 'Troubleshooting & FAQ',
        icon: HelpCircle,
        articlesCount: 40,
        description: 'Common error codes, debugging workflows, known limits, and frequent questions.',
        featuredTopics: ['Error Codes', 'Performance', 'Account Recovery'],
      },
    ]
    
    const defaultArticles: Article[] = [
      {
        id: 'art-1',
        title: 'Setting up SAML 2.0 Single Sign-On (SSO)',
        description: 'Step-by-step instructions for configuring Okta, Azure AD, and Google Workspace SSO.',
        category: 'Security',
        readTime: '6 min read',
        viewCount: '14.2k views',
      },
      {
        id: 'art-2',
        title: 'Migrating from v1 REST API to v2 GraphQL',
        description: 'Breaking changes, schema transformations, and backward-compatibility guidelines.',
        category: 'API & SDKs',
        readTime: '8 min read',
        viewCount: '11.8k views',
      },
      {
        id: 'art-3',
        title: 'Configuring Custom Domains & SSL Certificates',
        description: 'DNS record configuration, automatic TLS provisioning, and apex domain routing.',
        category: 'Getting Started',
        readTime: '4 min read',
        viewCount: '9.5k views',
      },
      {
        id: 'art-4',
        title: 'Webhook Signatures & Replay Attack Prevention',
        description: 'Verify HMAC-SHA256 signatures and implement idempotency keys for event handling.',
        category: 'Integrations',
        readTime: '5 min read',
        viewCount: '8.9k views',
      },
      {
        id: 'art-5',
        title: 'Managing Team Roles & Granular Permissions',
        description: 'Assigning RBAC policies, custom permission sets, and project-level scopes.',
        category: 'Account',
        readTime: '5 min read',
        viewCount: '7.4k views',
      },
      {
        id: 'art-6',
        title: 'Debugging 429 Rate Limit Errors & Backoff Strategies',
        description: 'Understanding quota tiers, exponential backoff algorithms, and jitter implementations.',
        category: 'Troubleshooting',
        readTime: '7 min read',
        viewCount: '12.1k views',
      },
    ]
    
    const quickTopics = ['Authentication', 'Billing', 'Webhooks', 'Custom Domains']
    
    @Component({
      changeDetection: ChangeDetectionStrategy.Eager,
      selector: 'ui-knowledge-base-hub, [ui-knowledge-base-hub]',
      standalone: true,
      // React renders the root <div> itself: the host stays out of layout and `class` goes to the root.
      host: { '[attr.class]': '"contents"' },
      imports: [
        LucideAngularModule,
        UiBadgeComponent,
        UiButtonComponent,
        UiCardComponent,
        UiCardContentComponent,
        UiCardDescriptionComponent,
        UiCardFooterComponent,
        UiCardHeaderComponent,
        UiCardTitleComponent,
        UiInputComponent,
      ],
      template: `
        <div data-slot="knowledge-base-hub" [class]="rootClass">
          <!-- Hero Header -->
          <section class="mx-auto max-w-3xl space-y-6 text-center">
            <div class="space-y-3">
              <span ui-badge variant="secondary" class="gap-1.5 px-3 py-1 font-medium">
                <lucide-icon [img]="BookOpen" class="text-primary size-3.5" />
                Help Center & Documentation
              </span>
              <h1 class="text-3xl font-bold tracking-tight sm:text-4xl lg:text-5xl">How can we help you today?</h1>
              <p class="text-muted-foreground mx-auto max-w-xl text-base sm:text-lg">
                Search our knowledge base for guides, API references, troubleshooting tips, and common workflows.
              </p>
            </div>
    
            <div class="relative mx-auto max-w-2xl">
              <lucide-icon
                [img]="Search"
                class="text-muted-foreground pointer-events-none absolute top-1/2 left-4 size-5 -translate-y-1/2"
              />
              <ui-input
                [value]="searchQuery()"
                (valueChange)="searchQuery.set($event)"
                type="search"
                placeholder="Search for answers, guides, and error codes..."
                class="bg-card h-12 rounded-xl pr-14 pl-11 text-base shadow-xs"
              />
              <div class="pointer-events-none absolute top-1/2 right-3 -translate-y-1/2">
                <kbd
                  class="bg-muted text-muted-foreground border-border hidden h-6 items-center rounded border px-2 font-mono text-xs select-none sm:inline-flex"
                >
                  ⌘K
                </kbd>
              </div>
            </div>
    
            <div class="flex flex-wrap items-center justify-center gap-2">
              <span class="text-muted-foreground text-xs font-medium">Quick topics:</span>
              @for (topic of topics; track topic) {
                <button type="button" [class]="topicPillClass(topic)" (click)="selectTopic(topic)">
                  {{ topic }}
                </button>
              }
              @if (searchQuery()) {
                <button
                  aria-label="Clear search"
                  type="button"
                  class="text-muted-foreground hover:text-foreground inline-flex items-center gap-1 px-1.5 py-1 text-xs font-medium transition-colors"
                  (click)="searchQuery.set('')"
                >
                  <lucide-icon [img]="X" class="size-3" />
                  Clear search
                </button>
              }
            </div>
          </section>
    
          <!-- Empty Search State -->
          @if (filteredCategories().length === 0 && filteredArticles().length === 0) {
            <div class="border-border bg-card/40 rounded-xl border border-dashed p-10 text-center">
              <div class="bg-muted text-muted-foreground mx-auto flex size-12 items-center justify-center rounded-full">
                <lucide-icon [img]="Search" class="size-6" />
              </div>
              <h3 class="mt-4 text-base font-semibold">No matching articles found</h3>
              <p class="text-muted-foreground mt-1 text-sm">
                No guides or categories matched &ldquo;{{ searchQuery() }}&rdquo;. Try searching for another topic.
              </p>
              <button
                ui-button
                aria-label="Clear search"
                variant="outline"
                size="sm"
                class="mt-4"
                (click)="searchQuery.set('')"
              >
                Reset search
              </button>
            </div>
          }
    
          <!-- Category Cards Grid -->
          @if (filteredCategories().length > 0) {
            <section class="space-y-4">
              <div class="flex items-center justify-between">
                <h2 class="text-xl font-semibold tracking-tight sm:text-2xl">Browse by Category</h2>
                <span class="text-muted-foreground text-xs tabular-nums">{{ filteredCategories().length }} categories</span>
              </div>
    
              <div class="grid gap-5 sm:grid-cols-2 lg:grid-cols-3">
                @for (category of filteredCategories(); track category.id) {
                  <div
                    ui-card
                    class="group/card hover:border-ring/50 relative flex flex-col justify-between overflow-hidden transition-colors duration-200 hover:shadow-sm"
                  >
                    <div ui-card-header class="pb-3">
                      <div class="flex items-start justify-between gap-3">
                        <div class="bg-primary/10 text-primary flex size-11 items-center justify-center rounded-lg">
                          <lucide-icon [img]="categoryIcon(category)" class="size-5" />
                        </div>
                        <span ui-badge variant="secondary" class="text-xs font-normal">
                          {{ category.articlesCount }} articles
                        </span>
                      </div>
                      <h3 ui-card-title class="mt-3 text-lg font-semibold tracking-tight">{{ category.title }}</h3>
                      <p ui-card-description class="text-muted-foreground text-sm leading-relaxed">
                        {{ category.description }}
                      </p>
                    </div>
                    <div ui-card-content class="pt-0">
                      <div class="border-border flex flex-wrap gap-1.5 border-t pt-3">
                        @for (topic of category.featuredTopics; track topic) {
                          <span class="bg-muted/60 text-muted-foreground rounded-md px-2 py-0.5 text-xs font-medium">
                            {{ topic }}
                          </span>
                        }
                      </div>
                    </div>
                    <div ui-card-footer class="border-border bg-muted/20 flex items-center justify-between border-t py-3">
                      <a
                        href="#"
                        class="text-primary group-hover/card:text-primary/80 flex min-h-6 items-center gap-1 text-xs font-medium transition-colors"
                      >
                        <span>Explore category</span>
                        <lucide-icon
                          [img]="ArrowRight"
                          class="size-3.5 transition-transform duration-200 group-hover/card:translate-x-1"
                        />
                      </a>
                    </div>
                  </div>
                }
              </div>
            </section>
          }
    
          <!-- Popular & Trending Articles Section -->
          @if (filteredArticles().length > 0) {
            <section class="space-y-4">
              <div class="flex flex-col justify-between gap-2 sm:flex-row sm:items-end">
                <div>
                  <h2 class="text-xl font-semibold tracking-tight sm:text-2xl">Popular & Trending Articles</h2>
                  <p class="text-muted-foreground mt-1 text-sm">
                    Frequently read guides and developer documentation this week.
                  </p>
                </div>
                <a
                  href="#"
                  class="text-primary hover:text-primary/80 inline-flex min-h-6 items-center gap-1 text-xs font-medium transition-colors"
                >
                  <span>View all documentation</span>
                  <lucide-icon [img]="ArrowRight" class="size-3.5" />
                </a>
              </div>
    
              <div class="grid gap-4 md:grid-cols-2">
                @for (article of filteredArticles(); track article.id) {
                  <div
                    ui-card
                    class="group/article hover:border-ring/50 relative flex flex-col justify-between p-5 transition-colors duration-200 hover:shadow-sm"
                  >
                    <div class="space-y-2.5">
                      <div class="flex items-center justify-between gap-2">
                        <span ui-badge variant="outline" class="text-xs font-medium">{{ article.category }}</span>
                        <div class="text-muted-foreground flex items-center gap-3 text-xs">
                          <span class="inline-flex items-center gap-1">
                            <lucide-icon [img]="Clock" class="size-3.5" />
                            {{ article.readTime }}
                          </span>
                          <span class="inline-flex items-center gap-1">
                            <lucide-icon [img]="Eye" class="size-3.5" />
                            {{ article.viewCount }}
                          </span>
                        </div>
                      </div>
                      <div>
                        <a
                          href="#"
                          class="text-foreground group-hover/article:text-primary flex items-start justify-between gap-2 text-base font-semibold tracking-tight transition-colors"
                        >
                          <span>{{ article.title }}</span>
                          <lucide-icon
                            [img]="ArrowUpRight"
                            class="text-muted-foreground group-hover/article:text-primary size-4 shrink-0 transition-transform duration-200 group-hover/article:translate-x-0.5 group-hover/article:-translate-y-0.5"
                          />
                        </a>
                        <p class="text-muted-foreground mt-1 line-clamp-2 text-sm leading-relaxed">
                          {{ article.description }}
                        </p>
                      </div>
                    </div>
                  </div>
                }
              </div>
            </section>
          }
    
          <!-- Community & Live Support Footer Banner -->
          <div ui-card class="border-border bg-card/60 overflow-hidden shadow-xs">
            <div
              ui-card-content
              class="flex flex-col items-center justify-between gap-6 p-8 text-center sm:p-10 md:flex-row md:text-left"
            >
              <div class="max-w-xl space-y-2">
                <h3 class="text-xl font-semibold tracking-tight sm:text-2xl">
                  Can&apos;t find what you&apos;re looking for?
                </h3>
                <p class="text-muted-foreground text-sm leading-relaxed">
                  Our support engineers are available 24/7 to help resolve technical issues, or join our Discord community
                  to connect with other developers.
                </p>
              </div>
              <div class="flex shrink-0 flex-wrap items-center justify-center gap-3">
                <button ui-button class="gap-2">
                  <lucide-icon [img]="MessageSquare" class="size-4" />
                  <span>Contact Support</span>
                </button>
                <button ui-button variant="outline" class="gap-2">
                  <lucide-icon [img]="Users" class="size-4" />
                  <span>Join Discord Community</span>
                </button>
              </div>
            </div>
          </div>
        </div>
      `,
    })
    export class UiKnowledgeBaseHubComponent {
      protected readonly ArrowRight = ArrowRight
      protected readonly ArrowUpRight = ArrowUpRight
      protected readonly BookOpen = BookOpen
      protected readonly Clock = Clock
      protected readonly Eye = Eye
      protected readonly MessageSquare = MessageSquare
      protected readonly Search = Search
      protected readonly Users = Users
      protected readonly X = X
    
      protected readonly topics = quickTopics
    
      @Input() set initialSearch(v: string) {
        this.searchQuery.set(v ?? '')
      }
      @Input('class') className?: string
    
      readonly categories = signal<Category[]>([...defaultCategories])
      readonly articles = signal<Article[]>([...defaultArticles])
      readonly searchQuery = signal('')
    
      readonly filteredCategories = computed(() => {
        const q = this.searchQuery().trim().toLowerCase()
        if (!q) return this.categories()
        return this.categories().filter(
          (c) =>
            c.title.toLowerCase().includes(q) ||
            c.description.toLowerCase().includes(q) ||
            c.featuredTopics.some((t) => t.toLowerCase().includes(q)),
        )
      })
    
      readonly filteredArticles = computed(() => {
        const q = this.searchQuery().trim().toLowerCase()
        if (!q) return this.articles()
        return this.articles().filter(
          (a) =>
            a.title.toLowerCase().includes(q) ||
            a.description.toLowerCase().includes(q) ||
            a.category.toLowerCase().includes(q),
        )
      })
    
      get rootClass(): string {
        return cn('w-full space-y-12 py-6', this.className)
      }
    
      categoryIcon(category: Category): unknown {
        return category.icon
      }
    
      topicPillClass(topic: string): string {
        const active = this.searchQuery() === topic
        return cn(
          'focus-visible:ring-ring inline-flex items-center rounded-full border px-3 py-1 text-xs font-medium transition-colors focus-visible:ring-2 focus-visible:outline-none',
          active
            ? 'bg-primary text-primary-foreground border-primary'
            : 'bg-muted/50 text-muted-foreground hover:bg-muted hover:text-foreground border-transparent',
        )
      }
    
      selectTopic(topic: string): void {
        this.searchQuery.set(this.searchQuery() === topic ? '' : topic)
      }
    }
    
  • src/app/components/blocks/knowledge-base-hub/index.ts0.1 kB

Raw manifest:https://uipkge.dev/r/angular/knowledge-base-hub.json