Menu

Change language

GitHub repo

Sales Contact Form

blockmarketing

Interactive enterprise solutions architect dispatch workbench with track selection, live hub timezones, SLA badges, and ticket generator.

Installation

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

Variants

Loading interactive previews…

Schema

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

InquiryTrack
interface InquiryTrack {
  id: string
  title: string
  subtitle: string
  sla: string
  icon: 'enterprise' | 'support' | 'security'
}
ContactSubmitPayload
interface ContactSubmitPayload {
  type: string
  name: string
  email: string
  company: string
  teamSize: string
  message: string
  ticketId: string
}

Files installed (2)

  • src/app/components/blocks/contact-01/contact-01.component.ts17.6 kB
    // Interactive enterprise solutions architect dispatch workbench with track selection,
    // live hub timezones, SLA badges, and ticket generator. Port of the React block 1:1.
    import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output, computed, signal } from '@angular/core'
    import {
      Building2,
      Check,
      Copy,
      Headphones,
      LucideAngularModule,
      MessageSquare,
      Send,
      ShieldCheck,
    } 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,
      UiCardHeaderComponent,
      UiCardTitleComponent,
    } from '@/ui/card/card.component'
    import { UiInputComponent } from '@/ui/input/input.component'
    import { UiLabelComponent } from '@/ui/label/label.component'
    import { UiSeparatorComponent } from '@/ui/separator/separator.component'
    import { UiTextareaComponent } from '@/ui/textarea/textarea.component'
    
    export interface InquiryTrack {
      id: string
      title: string
      subtitle: string
      sla: string
      icon: 'enterprise' | 'support' | 'security'
    }
    
    export interface ContactSubmitPayload {
      type: string
      name: string
      email: string
      company: string
      teamSize: string
      message: string
      ticketId: string
    }
    
    const inquiryTypes: InquiryTrack[] = [
      {
        id: 'enterprise',
        title: 'Enterprise Architecture',
        subtitle: 'Custom registries, zero-lockin migrations & multi-team governance',
        sla: '< 2h SLA',
        icon: 'enterprise',
      },
      {
        id: 'support',
        title: 'Dedicated Engineering SLA',
        subtitle: '24/7 incident response, custom upstream bugfixes & private Slack channel',
        sla: '< 15m SLA',
        icon: 'support',
      },
      {
        id: 'security',
        title: 'Security & Compliance Review',
        subtitle: 'SOC2 Type II audits, self-hosted registry mirrors & air-gapped deployments',
        sla: '< 4h SLA',
        icon: 'security',
      },
    ]
    
    const globalHubs = [
      { city: 'San Francisco', tz: 'PST (UTC-8)', status: 'Active (09:00 - 18:00)', email: '[email protected]' },
      { city: 'London', tz: 'GMT (UTC+0)', status: 'Active (08:30 - 17:30)', email: '[email protected]' },
      { city: 'Singapore', tz: 'SGT (UTC+8)', status: 'Active (09:00 - 18:00)', email: '[email protected]' },
    ]
    
    const teamSizes = ['1-10', '10-50', '50+']
    
    @Component({
      changeDetection: ChangeDetectionStrategy.Eager,
      selector: 'ui-contact-01, [ui-contact-01]',
      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,
        UiCardDescriptionComponent,
        UiCardHeaderComponent,
        UiCardTitleComponent,
        UiInputComponent,
        UiLabelComponent,
        UiSeparatorComponent,
        UiTextareaComponent,
      ],
      template: `
        <section data-slot="contact-01" [class]="rootClass">
          <div class="mx-auto max-w-6xl space-y-12">
            <!-- Section Header -->
            <div class="max-w-3xl space-y-3">
              <span ui-badge variant="secondary" class="gap-1.5 px-3 py-1 font-mono text-xs shadow-xs">
                <lucide-icon [img]="ShieldCheck" class="text-primary size-3.5" />
                Enterprise Engineering & Architecture Solutions
              </span>
              <h2 class="text-foreground text-3xl font-bold tracking-tight sm:text-4xl">
                Talk to a principal design engineer.
              </h2>
              <p class="text-muted-foreground text-sm leading-relaxed sm:text-base">
                No generic sales reps. Connect directly with the core team that architects our dual-framework registry,
                token engines, and headless workbenches.
              </p>
            </div>
    
            <!-- Main Layout -->
            <div class="grid grid-cols-1 items-start gap-8 lg:grid-cols-12">
              <!-- Left Column: Inquiry Routing & Global Hubs (5 Cols) -->
              <div class="space-y-6 lg:col-span-5">
                <div class="space-y-3">
                  <p class="text-muted-foreground font-mono text-xs tracking-wider uppercase">Select Engagement Track</p>
                  <div class="space-y-2.5">
                    @for (track of inquiryTypes; track track.id) {
                      <div
                        role="button"
                        tabindex="0"
                        [class]="trackCardClass(track.id)"
                        (click)="selectedType.set(track.id)"
                        (keydown.enter)="selectedType.set(track.id)"
                        (keydown.space)="$event.preventDefault(); selectedType.set(track.id)"
                      >
                        <div class="flex items-start justify-between gap-3">
                          <div class="flex items-center gap-2.5">
                            <div [class]="trackIconClass(track.id)">
                              @switch (track.icon) {
                                @case ('enterprise') {
                                  <lucide-icon [img]="Building2" class="size-4" />
                                }
                                @case ('support') {
                                  <lucide-icon [img]="Headphones" class="size-4" />
                                }
                                @default {
                                  <lucide-icon [img]="ShieldCheck" class="size-4" />
                                }
                              }
                            </div>
                            <span class="text-foreground text-xs font-semibold">{{ track.title }}</span>
                          </div>
                          <span ui-badge variant="outline" class="bg-background font-mono text-xs">
                            {{ track.sla }}
                          </span>
                        </div>
                        <p class="text-muted-foreground mt-2 pl-9 text-xs">
                          {{ track.subtitle }}
                        </p>
                      </div>
                    }
                  </div>
                </div>
    
                <div ui-separator></div>
    
                <!-- Global Timezone & Architecture Hubs -->
                <div class="space-y-3">
                  <div class="flex items-center justify-between">
                    <p class="text-muted-foreground font-mono text-xs tracking-wider uppercase">Direct Engineering Hubs</p>
                    <div class="text-success flex items-center gap-1.5 font-mono text-xs">
                      <span class="bg-success size-2 animate-pulse rounded-full"></span>
                      Live Dispatch Active
                    </div>
                  </div>
    
                  <div class="space-y-2">
                    @for (hub of hubs; track hub.city) {
                      <div
                        class="border-border bg-card/60 flex items-center justify-between rounded-lg border p-3 text-xs shadow-xs"
                      >
                        <div>
                          <p class="text-foreground font-medium">{{ hub.city }}</p>
                          <p class="text-muted-foreground font-mono text-xs">{{ hub.tz }} &bull; {{ hub.status }}</p>
                        </div>
                        <a [href]="'mailto:' + hub.email" class="text-primary font-mono text-xs hover:underline">
                          {{ hub.email }}
                        </a>
                      </div>
                    }
                  </div>
                </div>
              </div>
    
              <!-- Right Column: Interactive Dispatch Form (7 Cols) -->
              <div class="lg:col-span-7">
                <div ui-card class="border-border bg-card overflow-hidden shadow-sm">
                  <div ui-card-header class="border-border/60 bg-muted/20 border-b pb-4">
                    <div class="flex items-center justify-between">
                      <h3 ui-card-title class="flex items-center gap-2 text-base font-semibold">
                        <lucide-icon [img]="MessageSquare" class="text-primary size-4" />
                        Dispatch Engineering Request
                      </h3>
                      <span ui-badge variant="secondary" class="font-mono text-xs">
                        Direct Dispatch: {{ selectedType() }}
                      </span>
                    </div>
                    <p ui-card-description class="text-xs">
                      Provide architectural specifications or questions. Our engineering team responds within guaranteed
                      SLA.
                    </p>
                  </div>
    
                  <div ui-card-content class="p-6">
                    @if (!sent()) {
                      <form class="space-y-4" (submit)="onFormSubmit($event)">
                        <div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
                          <div class="space-y-1.5">
                            <label ui-label for="contact-name" class="text-xs font-medium">Your Name</label>
                            <ui-input
                              id="contact-name"
                              [value]="name()"
                              (valueChange)="name.set($event)"
                              placeholder="Jane Doe"
                              class="h-9 text-xs"
                            />
                          </div>
                          <div class="space-y-1.5">
                            <label ui-label for="contact-email" class="text-xs font-medium">Work Email</label>
                            <ui-input
                              id="contact-email"
                              [value]="email()"
                              (valueChange)="email.set($event)"
                              type="email"
                              placeholder="[email protected]"
                              class="h-9 font-mono text-xs"
                            />
                          </div>
                        </div>
    
                        <div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
                          <div class="space-y-1.5">
                            <label ui-label for="contact-company" class="text-xs font-medium">Company / Organization</label>
                            <ui-input
                              id="contact-company"
                              [value]="company()"
                              (valueChange)="company.set($event)"
                              placeholder="Acme Corp"
                              class="h-9 text-xs"
                            />
                          </div>
                          <div class="space-y-1.5">
                            <span class="text-xs font-medium">Engineering Team Size</span>
                            <div class="grid grid-cols-3 gap-1.5">
                              @for (ts of teamSizes; track ts) {
                                <button type="button" [class]="teamSizeClass(ts)" (click)="teamSize.set(ts)">
                                  {{ ts }}
                                </button>
                              }
                            </div>
                          </div>
                        </div>
    
                        <div class="space-y-1.5">
                          <label ui-label for="contact-message" class="text-xs font-medium"
                            >Project Scope & Technical Details</label
                          >
                          <ui-textarea
                            id="contact-message"
                            [value]="message()"
                            (valueChange)="message.set($event)"
                            [rows]="4"
                            placeholder="Describe your tech stack (Vue 3, Nuxt 3, React 19, Next.js), design token requirements, and deployment targets..."
                            class="resize-none text-xs"
                          />
                        </div>
    
                        <div class="flex flex-col items-center justify-between gap-3 pt-2 sm:flex-row">
                          <div class="text-muted-foreground flex items-center gap-2 text-xs">
                            <lucide-icon [img]="ShieldCheck" class="text-success size-4" />
                            <span>NDA & IP protection pre-guaranteed.</span>
                          </div>
    
                          <button
                            ui-button
                            type="submit"
                            [disabled]="!canSubmit()"
                            class="h-9 w-full gap-1.5 px-5 text-xs font-semibold sm:w-auto"
                          >
                            <span>Dispatch Inquiry</span>
                            <lucide-icon [img]="Send" class="size-3.5" />
                          </button>
                        </div>
                      </form>
                    } @else {
                      <!-- Success Feedback View -->
                      <div class="space-y-5 px-4 py-8 text-center">
                        <div
                          class="bg-success/10 text-success mx-auto flex size-12 items-center justify-center rounded-full"
                        >
                          <lucide-icon [img]="Check" class="size-6" />
                        </div>
                        <div class="space-y-1">
                          <p class="text-foreground text-base font-bold">Inquiry Dispatched Successfully</p>
                          <p class="text-muted-foreground mx-auto max-w-sm text-xs">
                            A principal solutions architect has been assigned. You will receive a technical response at
                            <strong class="text-foreground font-mono">{{ email() }}</strong> within guaranteed SLA.
                          </p>
                        </div>
    
                        <div
                          class="border-border bg-muted/40 mx-auto flex max-w-xs items-center justify-between rounded-lg border p-3"
                        >
                          <div class="text-left">
                            <p class="text-muted-foreground font-mono text-xs uppercase">Reference Ticket ID</p>
                            <p class="text-foreground font-mono text-xs font-bold">{{ ticketId() }}</p>
                          </div>
                          <button
                            ui-button
                            variant="outline"
                            size="sm"
                            class="h-7 gap-1 font-mono text-xs"
                            (click)="copyTicket()"
                          >
                            @if (copiedTicket()) {
                              <lucide-icon [img]="Check" class="text-success size-3" />
                            } @else {
                              <lucide-icon [img]="Copy" class="size-3" />
                            }
                            <span>{{ copiedTicket() ? 'Copied' : 'Copy' }}</span>
                          </button>
                        </div>
    
                        <button
                          ui-button
                          variant="ghost"
                          size="sm"
                          class="text-muted-foreground text-xs"
                          (click)="sent.set(false)"
                        >
                          Send Another Inquiry
                        </button>
                      </div>
                    }
                  </div>
                </div>
              </div>
            </div>
          </div>
        </section>
      `,
    })
    export class UiContact01Component {
      protected readonly Building2 = Building2
      protected readonly Check = Check
      protected readonly Copy = Copy
      protected readonly Headphones = Headphones
      protected readonly MessageSquare = MessageSquare
      protected readonly Send = Send
      protected readonly ShieldCheck = ShieldCheck
    
      protected readonly inquiryTypes = inquiryTypes
      protected readonly hubs = globalHubs
      protected readonly teamSizes = teamSizes
    
      @Input('class') className?: string
      @Output() readonly submit = new EventEmitter<ContactSubmitPayload>()
    
      readonly selectedType = signal('enterprise')
      readonly name = signal('')
      readonly email = signal('')
      readonly company = signal('')
      readonly teamSize = signal('10-50')
      readonly message = signal('')
      readonly sent = signal(false)
      readonly ticketId = signal('')
      readonly copiedTicket = signal(false)
      private copyTimer: ReturnType<typeof setTimeout> | undefined
    
      readonly canSubmit = computed(() => !!this.name().trim() && !!this.email().trim() && !!this.message().trim())
    
      get rootClass(): string {
        return cn('bg-background relative overflow-hidden px-4 py-16 sm:px-6 lg:px-8', this.className)
      }
    
      trackCardClass(id: string): string {
        return cn(
          'group focus-visible:ring-ring cursor-pointer rounded-xl border p-4 transition-colors focus-visible:ring-2 focus-visible:outline-none',
          this.selectedType() === id
            ? 'border-primary bg-primary/5 ring-primary/20 shadow-xs ring-1'
            : 'border-border bg-card hover:bg-muted/40',
        )
      }
    
      trackIconClass(id: string): string {
        return cn(
          'rounded-lg p-2 transition-colors',
          this.selectedType() === id
            ? 'bg-primary text-primary-foreground'
            : 'bg-muted text-foreground group-hover:bg-primary/10 group-hover:text-primary',
        )
      }
    
      teamSizeClass(ts: string): string {
        return cn(
          'rounded-md border px-2 py-1.5 text-center font-mono text-xs transition-colors',
          this.teamSize() === ts
            ? 'border-primary bg-primary/10 text-primary font-semibold'
            : 'border-border bg-background text-muted-foreground hover:text-foreground',
        )
      }
    
      onFormSubmit(event: Event): void {
        event.preventDefault()
        this.dispatch()
      }
    
      dispatch(): ContactSubmitPayload | null {
        if (!this.canSubmit()) return null
        const generatedId = `UIP-${Math.floor(1000 + Math.random() * 9000)}`
        this.ticketId.set(generatedId)
        this.sent.set(true)
        const payload: ContactSubmitPayload = {
          type: this.selectedType(),
          name: this.name(),
          email: this.email(),
          company: this.company(),
          teamSize: this.teamSize(),
          message: this.message(),
          ticketId: generatedId,
        }
        this.submit.emit(payload)
        return payload
      }
    
      copyTicket(): void {
        const id = this.ticketId()
        if (!id) return
        try {
          void navigator.clipboard?.writeText(id)
        } catch {
          // Clipboard unavailable (e.g. jsdom) — still flip the label.
        }
        this.copiedTicket.set(true)
        clearTimeout(this.copyTimer)
        this.copyTimer = setTimeout(() => this.copiedTicket.set(false), 2000)
      }
    }
    
  • src/app/components/blocks/contact-01/index.ts0.1 kB

Raw manifest:https://uipkge.dev/r/angular/contact-01.json