Menu

Change language

GitHub repo

Certificate Credential Card

blockeducation

Digital diploma and verified credential card featuring ornamental certificate container, cryptographic seal with QR code verification, recipient honors, validated skill pills, instructor signatures, and social sharing.

Installation

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

Variants

Loading interactive previews…

npm dependencies

Files installed (2)

  • src/app/components/blocks/certificate-credential-card/certificate-credential-card.component.ts27.4 kB
    // Digital diploma and verified credential card: ornamental certificate container,
    // cryptographic seal with QR verification, honors distinction, validated skill pills,
    // instructor signatures, and social sharing. Port of the React block 1:1.
    import { ChangeDetectionStrategy, Component, DestroyRef, Input, inject, signal } from '@angular/core'
    import { Award, Check, Copy, Download, ExternalLink, LucideAngularModule, Share2, 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, UiCardHeaderComponent } from '@/ui/card/card.component'
    
    const credentialData = {
      certId: 'CERT-2026-984210',
      institution: 'UIPKGE Engineering Academy',
      motto: 'EXCELLENTIA IN ARCHITECTURA',
      type: 'Professional Certificate of Mastery in Full-Stack UI Engineering',
      recipientName: 'Elena Rostova',
      achievementDescription:
        'For demonstrating advanced mastery in headless component architecture, OKLCH token systems, accessibility compliance, and dual-framework engineering.',
      issueDate: 'August 21, 2026',
      expiration: 'No Expiration · Lifetime Validity',
      skills: ['Vue 3.5', 'React 19', 'TypeScript', 'Tailwind v4', 'Reka UI', 'WCAG AA A11y'],
      leadInstructor: {
        name: 'Dr. Marcus Vance',
        title: 'Lead Instructor & Distinguished Architect',
      },
      academicDean: {
        name: 'Sarah Jenkins, Ph.D.',
        title: 'Academic Dean of Engineering',
      },
      sha256Digest: '7f83b1657ff1fc53b92dc18148a1d65dfc2d4b1fa3d677284addd200126d9069',
      verifyUrl: 'https://uipkge.dev/verify/CERT-2026-984210',
      blockLedger: 'Block #19,482,019 (OpenAttestation v3)',
    }
    
    @Component({
      changeDetection: ChangeDetectionStrategy.Eager,
      selector: 'ui-certificate-credential-card, [ui-certificate-credential-card]',
      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,
        UiCardHeaderComponent,
      ],
      template: `
        <div data-slot="certificate-credential-card" [class]="rootClass">
          <!-- Header Card -->
          <div ui-card class="border shadow-xs">
            <div ui-card-header class="flex flex-col gap-4 pb-6 sm:flex-row sm:items-start sm:justify-between">
              <div class="space-y-2">
                <div class="flex flex-wrap items-center gap-2">
                  <span ui-badge wrap variant="outline" class="gap-1.5 font-mono text-xs">
                    <lucide-icon [img]="Award" class="text-warning size-3.5" aria-hidden="true" />
                    Verified Credential
                  </span>
                  <div
                    class="border-success/20 bg-success/10 text-success inline-flex items-center gap-1.5 rounded-full border px-2.5 py-0.5 text-xs font-medium"
                  >
                    <span class="relative flex size-1.5">
                      <span class="bg-success absolute inline-flex h-full w-full rounded-full opacity-75"></span>
                      <span class="bg-success relative inline-flex size-1.5 rounded-full"></span>
                    </span>
                    Authentic · Cryptographically Signed
                  </div>
                </div>
    
                <div>
                  <h1 class="text-foreground text-xl font-bold tracking-tight sm:text-2xl">
                    Verified Credential &amp; Certificate of Mastery
                  </h1>
                  <div class="text-muted-foreground mt-1 flex flex-wrap items-center gap-2 text-xs sm:text-sm">
                    <span>Credential ID:</span>
                    <button
                      type="button"
                      (click)="copyCertId()"
                      class="hover:bg-muted focus-visible:ring-ring border-border bg-muted/40 text-foreground inline-flex min-h-6 items-center gap-1.5 rounded border px-2 py-0.5 font-mono text-xs font-semibold transition-colors focus-visible:ring-2 focus-visible:outline-hidden"
                      title="Click to copy credential ID"
                    >
                      #{{ data.certId }}
                      @if (copiedCertId()) {
                        <lucide-icon [img]="Check" class="text-success size-3" aria-hidden="true" />
                      } @else {
                        <lucide-icon [img]="Copy" class="text-muted-foreground size-3" aria-hidden="true" />
                      }
                    </button>
                    <span class="text-muted-foreground tabular-nums">Issued {{ data.issueDate }}</span>
                  </div>
                </div>
              </div>
    
              <div class="flex flex-wrap items-center gap-2.5 pt-1">
                <button ui-button variant="default" (click)="handleLinkedInShare()" class="gap-2 shadow-xs">
                  <lucide-icon [img]="Share2" class="size-4" aria-hidden="true" />
                  {{ sharedLinkedIn() ? 'Shared!' : 'Share on LinkedIn' }}
                </button>
                <button
                  ui-button
                  aria-label="Download attachment"
                  variant="outline"
                  (click)="handleDownloadPdf()"
                  class="gap-2 shadow-xs"
                >
                  <lucide-icon [img]="Download" class="size-4" aria-hidden="true" />
                  {{ downloadingPdf() ? 'Generating PDF...' : 'Download High-Res PDF' }}
                </button>
              </div>
            </div>
          </div>
    
          <!-- Certificate Visual Diploma Frame -->
          <div
            ui-card
            class="border-border from-card via-card/95 to-card/90 relative overflow-hidden rounded-2xl border bg-gradient-to-b shadow-sm"
          >
            <!-- Watermark Background Graphic -->
            <div
              class="pointer-events-none absolute inset-0 flex items-center justify-center overflow-hidden opacity-[0.03] select-none dark:opacity-[0.05]"
              aria-hidden="true"
            >
              <svg
                class="h-[680px] w-full max-w-[680px]"
                viewBox="0 0 500 500"
                fill="none"
                xmlns="http://www.w3.org/2000/svg"
              >
                <circle cx="250" cy="250" r="230" stroke="currentColor" stroke-width="1.5" stroke-dasharray="4 4" />
                <circle cx="250" cy="250" r="210" stroke="currentColor" stroke-width="2" />
                <circle cx="250" cy="250" r="185" stroke="currentColor" stroke-width="1" stroke-dasharray="2 2" />
                <circle cx="250" cy="250" r="150" stroke="currentColor" stroke-width="1.5" />
                <circle cx="250" cy="250" r="120" stroke="currentColor" stroke-width="1" />
                <circle cx="250" cy="250" r="90" stroke="currentColor" stroke-width="2" stroke-dasharray="6 3" />
                <circle cx="250" cy="250" r="60" stroke="currentColor" stroke-width="1" />
                <g stroke="currentColor" stroke-width="0.75" opacity="0.6">
                  <line x1="250" y1="20" x2="250" y2="480" />
                  <line x1="20" y1="250" x2="480" y2="250" />
                  <line x1="87" y1="87" x2="413" y2="413" />
                  <line x1="87" y1="413" x2="413" y2="87" />
                  <line x1="135" y1="51" x2="365" y2="449" />
                  <line x1="365" y1="51" x2="135" y2="449" />
                  <line x1="51" y1="135" x2="449" y2="365" />
                  <line x1="51" y1="365" x2="449" y2="135" />
                </g>
                <polygon
                  points="250,170 275,220 330,225 290,265 300,320 250,290 200,320 210,265 170,225 225,220"
                  stroke="currentColor"
                  stroke-width="1.5"
                  fill="none"
                />
              </svg>
            </div>
    
            <div ui-card-content class="relative z-10 p-6 sm:p-10 lg:p-12">
              <div
                class="border-border/80 bg-background/50 relative rounded-xl border p-6 backdrop-blur-xs sm:p-10 lg:p-12"
              >
                <div
                  class="border-border/70 pointer-events-none absolute inset-3 rounded-lg border border-dashed sm:inset-4"
                >
                  <div class="text-warning/70 absolute -top-2.5 -left-2.5 flex size-5 items-center justify-center">
                    <svg class="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                      <path d="M12 2v20M2 12h20M5 5l14 14M5 19L19 5" />
                    </svg>
                  </div>
                  <div class="text-warning/70 absolute -top-2.5 -right-2.5 flex size-5 items-center justify-center">
                    <svg class="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                      <path d="M12 2v20M2 12h20M5 5l14 14M5 19L19 5" />
                    </svg>
                  </div>
                  <div class="text-warning/70 absolute -bottom-2.5 -left-2.5 flex size-5 items-center justify-center">
                    <svg class="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                      <path d="M12 2v20M2 12h20M5 5l14 14M5 19L19 5" />
                    </svg>
                  </div>
                  <div class="text-warning/70 absolute -right-2.5 -bottom-2.5 flex size-5 items-center justify-center">
                    <svg class="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                      <path d="M12 2v20M2 12h20M5 5l14 14M5 19L19 5" />
                    </svg>
                  </div>
                </div>
    
                <div class="relative space-y-8 text-center sm:space-y-10">
                  <div class="flex flex-col items-center space-y-3">
                    <div class="relative flex size-20 items-center justify-center sm:size-24">
                      <div
                        class="to-primary/20 absolute inset-0 rounded-full bg-gradient-to-tr from-amber-500/20 via-emerald-500/20 blur-md"
                      ></div>
                      <div
                        class="bg-card border-warning/40 relative flex size-18 items-center justify-center rounded-full border-2 p-3 shadow-md sm:size-20"
                      >
                        <svg
                          class="text-foreground size-full"
                          viewBox="0 0 100 100"
                          fill="none"
                          xmlns="http://www.w3.org/2000/svg"
                          aria-label="UIPKGE Engineering Academy Crest"
                        >
                          <path
                            d="M50 8 L84 20 V50 C84 72 50 92 50 92 C50 92 16 72 16 50 V20 Z"
                            fill="currentColor"
                            fill-opacity="0.06"
                            stroke="currentColor"
                            stroke-width="3"
                            stroke-linejoin="round"
                          />
                          <path
                            d="M50 16 L76 26 V48 C76 66 50 82 50 82 C50 82 24 66 24 48 V26 Z"
                            stroke="currentColor"
                            stroke-width="1.5"
                            stroke-dasharray="3 2"
                          />
                          <ellipse
                            cx="50"
                            cy="48"
                            rx="20"
                            ry="8"
                            stroke="currentColor"
                            stroke-width="1.5"
                            transform="rotate(-30 50 48)"
                          />
                          <ellipse
                            cx="50"
                            cy="48"
                            rx="20"
                            ry="8"
                            stroke="currentColor"
                            stroke-width="1.5"
                            transform="rotate(30 50 48)"
                          />
                          <circle cx="50" cy="48" r="4" fill="currentColor" />
                          <polygon
                            points="50,30 54,38 63,39 56,45 58,54 50,49 42,54 44,45 37,39 46,38"
                            fill="currentColor"
                            fill-opacity="0.8"
                          />
                        </svg>
                      </div>
                    </div>
    
                    <div class="space-y-1">
                      <p class="text-muted-foreground text-xs font-bold tracking-[0.25em] uppercase">
                        {{ data.institution }}
                      </p>
                      <p class="text-success font-mono text-xs font-semibold tracking-widest">{{ data.motto }}</p>
                    </div>
    
                    <div class="text-muted-foreground/60 flex items-center justify-center gap-3 pt-1">
                      <span class="bg-border h-px w-12 sm:w-20"></span>
                      <lucide-icon [img]="Award" class="text-warning/80 size-3.5" aria-hidden="true" />
                      <span class="font-mono text-xs tracking-wider uppercase">Accredited Diploma</span>
                      <lucide-icon [img]="Award" class="text-warning/80 size-3.5" aria-hidden="true" />
                      <span class="bg-border h-px w-12 sm:w-20"></span>
                    </div>
                  </div>
    
                  <div class="space-y-4">
                    <p class="text-muted-foreground text-xs font-medium tracking-[0.2em] uppercase">
                      This is to certify that
                    </p>
                    <h2 class="text-foreground text-3xl font-bold tracking-tight sm:text-5xl lg:text-6xl">
                      {{ data.recipientName }}
                    </h2>
                    <p class="text-muted-foreground text-xs italic sm:text-sm">
                      has successfully fulfilled all rigorous requirements and demonstrated distinguished mastery in
                    </p>
                    <div class="border-primary/20 bg-primary/5 mx-auto max-w-2xl rounded-lg border px-4 py-3 sm:px-6">
                      <h3 class="text-foreground text-base font-semibold tracking-tight sm:text-xl md:text-2xl">
                        {{ data.type }}
                      </h3>
                    </div>
                    <p class="text-muted-foreground mx-auto max-w-2xl text-xs leading-relaxed sm:text-sm">
                      {{ data.achievementDescription }}
                    </p>
                    <div
                      class="border-warning/30 bg-warning/10 text-warning inline-flex items-center gap-2 rounded-full border px-3.5 py-1 text-xs font-medium"
                    >
                      <lucide-icon [img]="Award" class="size-3.5" aria-hidden="true" />
                      <span>Conferred with Highest Distinction · Top 1% Cohort · 480 CEU Hours</span>
                    </div>
                  </div>
    
                  <div class="space-y-3 pt-2">
                    <p class="text-muted-foreground text-xs font-medium">
                      Validated Competencies &amp; Technical Proficiencies
                    </p>
                    <div class="flex flex-wrap items-center justify-center gap-2">
                      @for (skill of data.skills; track skill) {
                        <span
                          ui-badge
                          wrap
                          variant="secondary"
                          class="border-border/80 bg-muted/60 hover:bg-muted gap-1.5 border px-3 py-1 text-xs font-medium transition-colors"
                        >
                          <span class="bg-success size-1.5 rounded-full"></span>
                          {{ skill }}
                        </span>
                      }
                    </div>
                  </div>
    
                  <div
                    class="border-border/60 bg-muted/20 mx-auto grid max-w-xl grid-cols-1 gap-3 rounded-lg border p-3 text-xs sm:grid-cols-3"
                  >
                    <div>
                      <span class="text-muted-foreground">Issue Date</span>
                      <p class="text-foreground mt-0.5 font-semibold">{{ data.issueDate }}</p>
                    </div>
                    <div class="border-border/60 border-t pt-2 sm:border-t-0 sm:border-l sm:pt-0 sm:pl-3">
                      <span class="text-muted-foreground">Validity</span>
                      <p class="text-success mt-0.5 font-semibold">{{ data.expiration }}</p>
                    </div>
                    <div class="border-border/60 border-t pt-2 sm:border-t-0 sm:border-l sm:pt-0 sm:pl-3">
                      <span class="text-muted-foreground">Credential Status</span>
                      <p class="text-foreground mt-0.5 font-mono font-semibold">Active · Verified</p>
                    </div>
                  </div>
    
                  <div class="grid grid-cols-1 items-end gap-8 pt-4 sm:grid-cols-3 sm:gap-6">
                    <div class="flex flex-col items-center space-y-2 text-center">
                      <div class="flex h-14 w-44 items-center justify-center">
                        <svg
                          class="text-foreground h-full w-full opacity-90"
                          viewBox="0 0 200 60"
                          fill="none"
                          xmlns="http://www.w3.org/2000/svg"
                          aria-label="Signature of Dr. Marcus Vance"
                        >
                          <path
                            d="M 15,45 C 18,20 28,12 36,25 C 44,38 48,15 56,22 C 64,29 68,48 76,42 C 84,36 92,30 102,32 C 114,34 122,46 134,36 C 144,28 152,18 162,26 C 172,34 180,42 188,40"
                            stroke="currentColor"
                            stroke-width="2.2"
                            stroke-linecap="round"
                            stroke-linejoin="round"
                          />
                          <path
                            d="M 30,52 C 70,50 140,53 180,48"
                            stroke="currentColor"
                            stroke-width="1.2"
                            stroke-linecap="round"
                            stroke-dasharray="4 2"
                            opacity="0.6"
                          />
                        </svg>
                      </div>
                      <div class="border-border w-full max-w-[200px] border-t pt-1.5">
                        <p class="text-foreground text-xs font-bold">{{ data.leadInstructor.name }}</p>
                        <p class="text-muted-foreground text-xs">{{ data.leadInstructor.title }}</p>
                      </div>
                    </div>
    
                    <div class="flex flex-col items-center space-y-3">
                      <div class="relative flex flex-col items-center">
                        <div
                          class="via-background border-warning/60 relative flex size-28 flex-col items-center justify-center rounded-full border-2 bg-gradient-to-br from-amber-500/10 to-emerald-500/10 p-2 shadow-md"
                        >
                          <svg
                            class="text-foreground size-16"
                            viewBox="0 0 21 21"
                            fill="currentColor"
                            xmlns="http://www.w3.org/2000/svg"
                            aria-label="QR code for online credential verification"
                          >
                            <rect x="0" y="0" width="7" height="7" rx="0.5" />
                            <rect x="1" y="1" width="5" height="5" fill="var(--color-card, #fff)" />
                            <rect x="2" y="2" width="3" height="3" />
                            <rect x="14" y="0" width="7" height="7" rx="0.5" />
                            <rect x="15" y="1" width="5" height="5" fill="var(--color-card, #fff)" />
                            <rect x="16" y="2" width="3" height="3" />
                            <rect x="0" y="14" width="7" height="7" rx="0.5" />
                            <rect x="1" y="15" width="5" height="5" fill="var(--color-card, #fff)" />
                            <rect x="2" y="16" width="3" height="3" />
                            <rect x="8" y="6" width="1" height="1" />
                            <rect x="10" y="6" width="1" height="1" />
                            <rect x="12" y="6" width="1" height="1" />
                            <rect x="6" y="8" width="1" height="1" />
                            <rect x="6" y="10" width="1" height="1" />
                            <rect x="6" y="12" width="1" height="1" />
                            <rect x="8" y="8" width="2" height="2" />
                            <rect x="11" y="8" width="1" height="1" />
                            <rect x="13" y="9" width="2" height="1" />
                            <rect x="8" y="11" width="1" height="2" />
                            <rect x="10" y="11" width="2" height="1" />
                            <rect x="13" y="12" width="1" height="1" />
                            <rect x="14" y="8" width="1" height="1" />
                            <rect x="16" y="9" width="1" height="1" />
                            <rect x="19" y="10" width="1" height="1" />
                            <rect x="15" y="11" width="2" height="1" />
                            <rect x="18" y="12" width="1" height="1" />
                            <rect x="8" y="14" width="1" height="1" />
                            <rect x="10" y="15" width="2" height="1" />
                            <rect x="8" y="17" width="1" height="2" />
                            <rect x="10" y="18" width="1" height="1" />
                            <rect x="12" y="16" width="1" height="2" />
                            <rect x="12" y="19" width="1" height="1" />
                            <rect x="14" y="14" width="2" height="1" />
                            <rect x="17" y="15" width="1" height="1" />
                            <rect x="19" y="14" width="2" height="1" />
                            <rect x="15" y="17" width="1" height="2" />
                            <rect x="17" y="18" width="2" height="1" />
                            <rect x="20" y="19" width="1" height="1" />
                          </svg>
                          <div class="text-success mt-1 flex items-center gap-1">
                            <lucide-icon [img]="ShieldCheck" class="size-3" aria-hidden="true" />
                            <span class="font-mono text-xs font-semibold">VERIFIED</span>
                          </div>
                        </div>
                      </div>
                      <p class="text-muted-foreground text-xs font-medium">Scan to Verify Authenticity</p>
                    </div>
    
                    <div class="flex flex-col items-center space-y-2 text-center">
                      <div class="flex h-14 w-44 items-center justify-center">
                        <svg
                          class="text-foreground h-full w-full opacity-90"
                          viewBox="0 0 200 60"
                          fill="none"
                          xmlns="http://www.w3.org/2000/svg"
                          aria-label="Signature of Dr. Sarah Jenkins"
                        >
                          <path
                            d="M 20,25 C 24,14 34,12 40,20 C 46,28 32,48 42,48 C 52,48 60,30 70,30 C 80,30 84,42 94,40 C 104,38 112,24 122,24 C 132,24 136,44 148,42 C 158,40 166,28 176,32 C 182,35 186,40 190,38"
                            stroke="currentColor"
                            stroke-width="2.2"
                            stroke-linecap="round"
                            stroke-linejoin="round"
                          />
                          <path
                            d="M 25,54 C 65,52 130,50 175,48"
                            stroke="currentColor"
                            stroke-width="1.2"
                            stroke-linecap="round"
                            stroke-dasharray="4 2"
                            opacity="0.6"
                          />
                        </svg>
                      </div>
                      <div class="border-border w-full max-w-[200px] border-t pt-1.5">
                        <p class="text-foreground text-xs font-bold">{{ data.academicDean.name }}</p>
                        <p class="text-muted-foreground text-xs">{{ data.academicDean.title }}</p>
                      </div>
                    </div>
                  </div>
                </div>
              </div>
            </div>
          </div>
    
          <!-- Cryptographic Verification & Blockchain Ledger Audit Strip -->
          <div ui-card class="border-border bg-muted/20 border shadow-xs">
            <div ui-card-content class="flex flex-col gap-4 p-4 text-xs lg:flex-row lg:items-center lg:justify-between">
              <div class="text-muted-foreground flex items-center gap-2.5">
                <lucide-icon [img]="ShieldCheck" class="text-success size-4 shrink-0" aria-hidden="true" />
                <span>
                  Tamper-Proof Credential · W3C Verifiable Credentials &amp;
                  <span class="text-foreground font-mono font-medium">{{ data.blockLedger }}</span>
                </span>
              </div>
    
              <div class="flex flex-wrap items-center gap-3">
                <div class="flex items-center gap-1.5 font-mono">
                  <span class="text-muted-foreground">SHA-256:</span>
                  <button
                    type="button"
                    (click)="copyDigest()"
                    class="hover:bg-muted focus-visible:ring-ring border-border bg-muted/60 text-foreground inline-flex min-h-6 items-center gap-1 rounded border px-2 py-0.5 transition-colors focus-visible:ring-2 focus-visible:outline-hidden"
                    title="Click to copy SHA-256 cryptographic digest"
                  >
                    <span class="tabular-nums">7f83b1...126d9069</span>
                    @if (copiedDigest()) {
                      <lucide-icon [img]="Check" class="text-success size-3" aria-hidden="true" />
                    } @else {
                      <lucide-icon [img]="Copy" class="text-muted-foreground size-3" aria-hidden="true" />
                    }
                  </button>
                </div>
    
                <button
                  type="button"
                  (click)="copyVerifyUrl()"
                  class="hover:bg-muted focus-visible:ring-ring text-primary hover:text-primary/80 inline-flex min-h-6 items-center gap-1 rounded px-1.5 py-0.5 font-medium transition-colors focus-visible:ring-2 focus-visible:outline-hidden"
                  title="Click to copy public verification URL"
                >
                  <span>{{ copiedVerifyUrl() ? 'URL Copied' : 'Copy Verification URL' }}</span>
                  @if (copiedVerifyUrl()) {
                    <lucide-icon [img]="Check" class="text-success size-3" aria-hidden="true" />
                  } @else {
                    <lucide-icon [img]="ExternalLink" class="size-3" aria-hidden="true" />
                  }
                </button>
              </div>
            </div>
          </div>
        </div>
      `,
    })
    export class UiCertificateCredentialCardComponent {
      protected readonly Award = Award
      protected readonly Check = Check
      protected readonly Copy = Copy
      protected readonly Download = Download
      protected readonly ExternalLink = ExternalLink
      protected readonly Share2 = Share2
      protected readonly ShieldCheck = ShieldCheck
    
      protected readonly data = credentialData
    
      @Input('class') className?: string
    
      readonly copiedCertId = signal(false)
      readonly copiedVerifyUrl = signal(false)
      readonly copiedDigest = signal(false)
      readonly sharedLinkedIn = signal(false)
      readonly downloadingPdf = signal(false)
    
      private timers: ReturnType<typeof setTimeout>[] = []
    
      constructor() {
        inject(DestroyRef).onDestroy(() => this.timers.forEach((t) => clearTimeout(t)))
      }
    
      get rootClass(): string {
        return cn('w-full space-y-6', this.className)
      }
    
      private flash(flag: ReturnType<typeof signal<boolean>>, ms: number): void {
        flag.set(true)
        this.timers.push(setTimeout(() => flag.set(false), ms))
      }
    
      copyCertId(): void {
        try {
          void navigator.clipboard?.writeText(credentialData.certId)
        } catch {
          // Clipboard unavailable in some contexts; the check icon still confirms the intent.
        }
        this.flash(this.copiedCertId, 2000)
      }
    
      copyVerifyUrl(): void {
        try {
          void navigator.clipboard?.writeText(credentialData.verifyUrl)
        } catch {
          // ignore
        }
        this.flash(this.copiedVerifyUrl, 2000)
      }
    
      copyDigest(): void {
        try {
          void navigator.clipboard?.writeText(credentialData.sha256Digest)
        } catch {
          // ignore
        }
        this.flash(this.copiedDigest, 2000)
      }
    
      handleLinkedInShare(): void {
        const url = `https://www.linkedin.com/sharing/share-offsite/?url=${encodeURIComponent(credentialData.verifyUrl)}`
        try {
          window.open(url, '_blank', 'noopener,noreferrer')
        } catch {
          // ignore
        }
        this.flash(this.sharedLinkedIn, 2500)
      }
    
      handleDownloadPdf(): void {
        this.flash(this.downloadingPdf, 2000)
      }
    }
    
  • src/app/components/blocks/certificate-credential-card/index.ts0.1 kB

Raw manifest:https://uipkge.dev/r/angular/certificate-credential-card.json