Menu

Change language

GitHub repo

Careers Job Board

blockmarketing

Careers page and job openings board with perks overview, live department & location filtering, keyword search, grouped department openings, and general application CTA.

Installation

$npx uipkge-ng@latest add careers-job-board
Manifest:https://uipkge.dev/r/angular/careers-job-board.jsonInstalls to:src/app/components/blocks/careers-job-board/

Variants

Loading interactive previews…

Schema

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

Perk
interface Perk {
  icon: LucideIconData
  title: string
  description: string
}
JobOpening
interface JobOpening {
  id: string
  title: string
  department: 'Engineering' | 'Design' | 'Product' | 'Marketing'
  location: 'Remote' | 'San Francisco, CA' | 'London, UK'
  type: string
  level: string
  salary: string
  description: string
}

npm dependencies

Files installed (2)

  • src/app/components/blocks/careers-job-board/careers-job-board.component.ts18.1 kB
    // Careers page and job openings board with perks overview, live department &
    // location filtering, keyword search, grouped department openings, and a general
    // application CTA. Port of the React block 1:1.
    import { ChangeDetectionStrategy, Component, Input, computed, signal } from '@angular/core'
    import {
      ArrowRight,
      Briefcase,
      Coins,
      Globe,
      GraduationCap,
      HeartHandshake,
      LucideAngularModule,
      MapPin,
      Search,
      SearchX,
      TrendingUp,
      type LucideIconData,
    } from 'lucide-angular'
    import { cn } from '@/lib/utils'
    import { UiBadgeComponent } from '@/ui/badge/badge.component'
    import { UiButtonComponent } from '@/ui/button/button.component'
    import { UiInputComponent } from '@/ui/input/input.component'
    import {
      UiSelectComponent,
      UiSelectContentComponent,
      UiSelectItemComponent,
      UiSelectTriggerComponent,
      UiSelectValueComponent,
    } from '@/ui/select/select.component'
    
    interface Perk {
      icon: LucideIconData
      title: string
      description: string
    }
    
    export interface JobOpening {
      id: string
      title: string
      department: 'Engineering' | 'Design' | 'Product' | 'Marketing'
      location: 'Remote' | 'San Francisco, CA' | 'London, UK'
      type: string
      level: string
      salary: string
      description: string
    }
    
    const PERKS: Perk[] = [
      {
        icon: Globe,
        title: 'Remote-first',
        description: 'Work from anywhere in the world with flexible hours.',
      },
      {
        icon: GraduationCap,
        title: '$5k learning budget',
        description: 'Annual stipend for courses, books, tools, and conferences.',
      },
      {
        icon: HeartHandshake,
        title: '100% health coverage',
        description: 'Comprehensive medical, dental, and vision for you and dependents.',
      },
      {
        icon: TrendingUp,
        title: 'Equity packages',
        description: 'Meaningful ownership stake with transparent vesting schedules.',
      },
    ]
    
    const DEPARTMENTS = [
      { id: 'all', label: 'All', count: 12 },
      { id: 'Engineering', label: 'Engineering', count: 6 },
      { id: 'Design', label: 'Design', count: 2 },
      { id: 'Product', label: 'Product', count: 2 },
      { id: 'Marketing', label: 'Marketing', count: 2 },
    ] as const
    
    const LOCATIONS = [
      { id: 'all', label: 'All Locations' },
      { id: 'Remote', label: 'Remote' },
      { id: 'San Francisco, CA', label: 'San Francisco, CA' },
      { id: 'London, UK', label: 'London, UK' },
    ] as const
    
    const JOBS: JobOpening[] = [
      {
        id: 'eng-1',
        title: 'Staff Frontend Engineer',
        department: 'Engineering',
        location: 'Remote',
        type: 'Full-time',
        level: 'Staff',
        salary: '$190k - $240k · 0.3% - 0.6%',
        description:
          'Lead the architecture of our high-performance UI components, compiler toolchains, and multi-framework design systems.',
      },
      {
        id: 'eng-2',
        title: 'Senior Full-Stack Engineer',
        department: 'Engineering',
        location: 'San Francisco, CA',
        type: 'Full-time',
        level: 'Senior',
        salary: '$160k - $210k · 0.25% - 0.5%',
        description:
          'Build end-to-end features spanning serverless edge runtimes, component registries, and developer tooling.',
      },
      {
        id: 'eng-3',
        title: 'Systems & Performance Engineer',
        department: 'Engineering',
        location: 'Remote',
        type: 'Full-time',
        level: 'Senior',
        salary: '$170k - $220k · 0.25% - 0.5%',
        description:
          'Optimize bundling pipelines, AST transformers, and static site generation latency across large workspaces.',
      },
      {
        id: 'eng-4',
        title: 'Design Systems Engineer',
        department: 'Engineering',
        location: 'London, UK',
        type: 'Full-time',
        level: 'Mid-Senior',
        salary: '£110k - £140k · 0.2% - 0.4%',
        description:
          'Bridge design and code by maintaining tokens, accessible headless primitives, and fluid interaction standards.',
      },
      {
        id: 'eng-5',
        title: 'Developer Experience Engineer',
        department: 'Engineering',
        location: 'Remote',
        type: 'Full-time',
        level: 'Mid-Senior',
        salary: '$150k - $190k · 0.2% - 0.4%',
        description:
          'Craft exceptional CLI workflows, interactive documentation playgrounds, and starter template engines.',
      },
      {
        id: 'eng-6',
        title: 'QA & Automation Engineer',
        department: 'Engineering',
        location: 'London, UK',
        type: 'Full-time',
        level: 'Mid-Senior',
        salary: '£95k - £125k · 0.15% - 0.3%',
        description:
          'Expand cross-browser visual regression testing suites, accessibility audits, and automated release checks.',
      },
      {
        id: 'des-1',
        title: 'Senior Product Designer',
        department: 'Design',
        location: 'San Francisco, CA',
        type: 'Full-time',
        level: 'Senior',
        salary: '$155k - $195k · 0.25% - 0.45%',
        description:
          'Define micro-interactions, responsive dashboard workflows, and layout ergonomics across our block catalog.',
      },
      {
        id: 'des-2',
        title: 'Brand & Motion Designer',
        department: 'Design',
        location: 'Remote',
        type: 'Full-time',
        level: 'Mid-Senior',
        salary: '$140k - $180k · 0.2% - 0.35%',
        description:
          'Create fluid motion principles, brand identity assets, and polished launch visuals for major releases.',
      },
      {
        id: 'prd-1',
        title: 'Staff Technical Product Manager',
        department: 'Product',
        location: 'Remote',
        type: 'Full-time',
        level: 'Staff',
        salary: '$180k - $230k · 0.3% - 0.5%',
        description:
          'Drive product roadmap for developer workflows, unbundled registry distribution, and monorepo orchestration.',
      },
      {
        id: 'prd-2',
        title: 'Product Operations Lead',
        department: 'Product',
        location: 'San Francisco, CA',
        type: 'Full-time',
        level: 'Senior',
        salary: '$145k - $185k · 0.2% - 0.35%',
        description: 'Manage release triage, community feature requests, and cross-team execution cadences.',
      },
      {
        id: 'mkt-1',
        title: 'Developer Advocate / Technical Writer',
        department: 'Marketing',
        location: 'Remote',
        type: 'Full-time',
        level: 'Mid-Senior',
        salary: '$140k - $180k · 0.2% - 0.4%',
        description:
          'Author technical deep-dives, create interactive video tutorials, and engage with the open-source community.',
      },
      {
        id: 'mkt-2',
        title: 'Growth Marketing Lead',
        department: 'Marketing',
        location: 'London, UK',
        type: 'Full-time',
        level: 'Senior',
        salary: '£105k - £135k · 0.2% - 0.4%',
        description:
          'Lead developer-focused growth experiments, SEO architecture, launch campaigns, and analytics instrumentation.',
      },
    ]
    
    @Component({
      changeDetection: ChangeDetectionStrategy.Eager,
      selector: 'ui-careers-job-board, [ui-careers-job-board]',
      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,
        UiInputComponent,
        UiSelectComponent,
        UiSelectContentComponent,
        UiSelectItemComponent,
        UiSelectTriggerComponent,
        UiSelectValueComponent,
      ],
      template: `
        <section data-slot="careers-job-board" [class]="rootClass">
          <!-- Hero / Header -->
          <div class="mx-auto max-w-3xl space-y-4 text-center">
            <span ui-badge variant="secondary" class="gap-1.5 px-3 py-1 text-xs font-medium">
              <lucide-icon [img]="Briefcase" class="text-primary size-3.5" />
              We&apos;re hiring
            </span>
            <h1 class="text-foreground text-3xl font-bold tracking-tight sm:text-4xl lg:text-5xl">{{ titleInput() }}</h1>
            <p class="text-muted-foreground text-base leading-relaxed sm:text-lg">{{ subtitleInput() }}</p>
          </div>
    
          <!-- Perks summary row -->
          <div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
            @for (perk of perks; track perk.title) {
              <div class="bg-card border-border flex flex-col gap-3 rounded-xl border p-4 shadow-xs">
                <div class="bg-primary/10 text-primary flex size-9 items-center justify-center rounded-lg">
                  <lucide-icon [img]="perk.icon" class="size-4.5" />
                </div>
                <div class="space-y-1">
                  <p class="text-foreground text-sm font-semibold">{{ perk.title }}</p>
                  <p class="text-muted-foreground text-xs leading-relaxed">{{ perk.description }}</p>
                </div>
              </div>
            }
          </div>
    
          <!-- Filter Toolbar -->
          <div class="bg-card border-border space-y-4 rounded-xl border p-4 shadow-xs">
            <div class="flex flex-wrap items-center gap-1.5">
              @for (dept of departments; track dept.id) {
                <button
                  ui-button
                  [variant]="selectedDepartment() === dept.id ? 'default' : 'outline'"
                  size="sm"
                  class="h-8 rounded-lg text-xs"
                  (click)="selectedDepartment.set(dept.id)"
                >
                  {{ dept.label }}
                  <span [class]="deptCountClass(dept.id)">
                    {{ dept.count }}
                  </span>
                </button>
              }
            </div>
    
            <div class="flex flex-col gap-3 sm:flex-row sm:items-center">
              <div class="relative flex-1">
                <lucide-icon
                  [img]="Search"
                  class="text-muted-foreground pointer-events-none absolute top-1/2 left-3 size-4 -translate-y-1/2"
                />
                <ui-input
                  [value]="searchQuery()"
                  (valueChange)="searchQuery.set($event)"
                  placeholder="Search roles, technologies, or keywords..."
                  class="pl-9 text-sm"
                  aria-label="Search open positions"
                />
              </div>
    
              <div class="w-full shrink-0 sm:w-56">
                <ui-select [value]="selectedLocation()" (valueChange)="selectedLocation.set($event)">
                  <button ui-select-trigger class="w-full text-xs sm:text-sm" aria-label="Filter by location">
                    <ui-select-value placeholder="All Locations" />
                  </button>
                  <ui-select-content>
                    @for (loc of locations; track loc.id) {
                      <ui-select-item [value]="loc.id">{{ loc.label }}</ui-select-item>
                    }
                  </ui-select-content>
                </ui-select>
              </div>
    
              @if (isFiltered()) {
                <button
                  ui-button
                  variant="ghost"
                  size="sm"
                  class="text-muted-foreground hover:text-foreground text-xs"
                  (click)="resetFilters()"
                >
                  Reset filters
                </button>
              }
            </div>
          </div>
    
          <!-- Grouped Job Openings list -->
          @if (departmentGroups().length > 0) {
            <div class="space-y-10">
              @for (group of departmentGroups(); track group.name) {
                <div class="space-y-4">
                  <div class="border-border flex items-center justify-between border-b pb-3">
                    <div class="flex items-center gap-2.5">
                      <h2 class="text-foreground text-lg font-semibold tracking-tight">{{ group.name }}</h2>
                      <span ui-badge variant="secondary" class="text-xs font-normal">
                        {{ group.jobs.length }} {{ group.jobs.length === 1 ? 'opening' : 'openings' }}
                      </span>
                    </div>
                  </div>
    
                  <div class="space-y-3">
                    @for (job of group.jobs; track job.id) {
                      <article
                        class="group bg-card border-border hover:border-primary/40 relative flex flex-col justify-between gap-4 rounded-xl border p-5 shadow-xs transition-colors duration-150 hover:shadow-sm md:flex-row md:items-center"
                      >
                        <div class="min-w-0 flex-1 space-y-2">
                          <div class="flex flex-wrap items-center gap-2">
                            <h3
                              class="group-hover:text-primary text-foreground text-base font-semibold tracking-tight transition-colors sm:text-lg"
                            >
                              {{ job.title }}
                            </h3>
                            <span ui-badge variant="outline" class="text-xs">{{ job.level }}</span>
                          </div>
                          <p class="text-muted-foreground line-clamp-2 text-sm leading-relaxed">{{ job.description }}</p>
                          <div class="text-muted-foreground flex flex-wrap items-center gap-x-4 gap-y-2 pt-1 text-xs">
                            <span class="inline-flex items-center gap-1.5">
                              <lucide-icon [img]="MapPin" class="text-muted-foreground size-3.5" />
                              {{ job.location }}
                            </span>
                            <span class="inline-flex items-center gap-1.5">
                              <lucide-icon [img]="Briefcase" class="text-muted-foreground size-3.5" />
                              {{ job.type }}
                            </span>
                            <span class="text-foreground inline-flex items-center gap-1.5 font-medium">
                              <lucide-icon [img]="Coins" class="text-muted-foreground size-3.5" />
                              {{ job.salary }}
                            </span>
                          </div>
                        </div>
                        <div class="shrink-0 pt-2 md:pt-0">
                          <button ui-button class="w-full gap-1.5 md:w-auto" size="sm">
                            Apply now
                            <lucide-icon
                              [img]="ArrowRight"
                              class="size-3.5 transition-transform group-hover:translate-x-0.5"
                            />
                          </button>
                        </div>
                      </article>
                    }
                  </div>
                </div>
              }
            </div>
          } @else {
            <!-- Empty State -->
            <div
              class="border-border flex flex-col items-center justify-center rounded-xl border border-dashed p-12 text-center"
            >
              <div class="bg-muted text-muted-foreground mb-4 flex size-12 items-center justify-center rounded-full">
                <lucide-icon [img]="SearchX" class="size-6" />
              </div>
              <h3 class="text-foreground text-base font-semibold">No matching positions found</h3>
              <p class="text-muted-foreground mt-1 mb-4 max-w-sm text-sm leading-normal">
                We could not find any roles matching your current search or filters. Try adjusting your criteria.
              </p>
              <button ui-button variant="outline" size="sm" (click)="resetFilters()">Clear all filters</button>
            </div>
          }
    
          <!-- Don't see your role? CTA Card -->
          <div
            class="bg-card border-border flex flex-col items-start justify-between gap-6 rounded-2xl border p-6 shadow-xs sm:flex-row sm:items-center sm:p-8"
          >
            <div class="max-w-xl space-y-1.5">
              <h3 class="text-foreground text-lg font-semibold tracking-tight">Don&apos;t see your role?</h3>
              <p class="text-muted-foreground text-sm leading-relaxed">
                We are always looking for curious, high-craft engineers, designers, and thinkers. Send us an open
                application and tell us what you&apos;d love to build with us.
              </p>
            </div>
            <button ui-button variant="outline" class="shrink-0 gap-2">
              Send open application
              <lucide-icon [img]="ArrowRight" class="size-4" />
            </button>
          </div>
        </section>
      `,
    })
    export class UiCareersJobBoardComponent {
      protected readonly ArrowRight = ArrowRight
      protected readonly Briefcase = Briefcase
      protected readonly Coins = Coins
      protected readonly MapPin = MapPin
      protected readonly Search = Search
      protected readonly SearchX = SearchX
    
      protected readonly perks = PERKS
      protected readonly departments = DEPARTMENTS
      protected readonly locations = LOCATIONS
    
      readonly titleInput = signal('Join our team')
      readonly subtitleInput = signal('Help us build the open-source UI infrastructure of tomorrow')
    
      // React props are `title` / `subtitle`; `title` shadows the DOM property so the
      // signal keeps an internal name and the alias exposes the canonical binding.
      @Input({ alias: 'title' }) set titleProp(v: string | undefined) {
        if (v !== undefined) this.titleInput.set(v)
      }
      @Input({ alias: 'subtitle' }) set subtitleProp(v: string | undefined) {
        if (v !== undefined) this.subtitleInput.set(v)
      }
      @Input('class') className?: string
    
      readonly selectedDepartment = signal<string>('all')
      readonly selectedLocation = signal<string>('all')
      readonly searchQuery = signal('')
    
      readonly filteredJobs = computed(() => {
        const q = this.searchQuery().trim().toLowerCase()
        return JOBS.filter((job) => {
          const matchDept = this.selectedDepartment() === 'all' || job.department === this.selectedDepartment()
          const matchLoc = this.selectedLocation() === 'all' || job.location === this.selectedLocation()
          if (!matchDept || !matchLoc) return false
          if (!q) return true
          return (
            job.title.toLowerCase().includes(q) ||
            job.description.toLowerCase().includes(q) ||
            job.department.toLowerCase().includes(q) ||
            job.level.toLowerCase().includes(q) ||
            job.location.toLowerCase().includes(q) ||
            job.type.toLowerCase().includes(q) ||
            job.salary.toLowerCase().includes(q)
          )
        })
      })
    
      readonly departmentGroups = computed(() => {
        const allDepts = ['Engineering', 'Design', 'Product', 'Marketing'] as const
        const sel = this.selectedDepartment()
        const target = sel === 'all' ? allDepts : allDepts.filter((dept) => dept === sel)
        return target
          .map((dept) => ({ name: dept, jobs: this.filteredJobs().filter((job) => job.department === dept) }))
          .filter((group) => group.jobs.length > 0)
      })
    
      readonly isFiltered = computed(
        () => this.selectedDepartment() !== 'all' || this.selectedLocation() !== 'all' || this.searchQuery().trim() !== '',
      )
    
      get rootClass(): string {
        return cn('w-full space-y-12 py-6', this.className)
      }
    
      deptCountClass(id: string): string {
        return cn(
          'ml-1.5 rounded-full px-1.5 py-0.5 text-xs font-medium tabular-nums',
          this.selectedDepartment() === id
            ? 'bg-primary-foreground/20 text-primary-foreground'
            : 'bg-muted text-muted-foreground',
        )
      }
    
      resetFilters(): void {
        this.selectedDepartment.set('all')
        this.selectedLocation.set('all')
        this.searchQuery.set('')
      }
    }
    
  • src/app/components/blocks/careers-job-board/index.ts0.1 kB

Raw manifest:https://uipkge.dev/r/angular/careers-job-board.json