Menu

Change language

GitHub repo

Privacy Data Export Portal

blocklegal

GDPR Article 15 and CCPA personal data export portal with privacy governance stat cards, granular category export selector, format chooser, and export archive history table.

Installation

$npx uipkge-ng@latest add privacy-data-export-portal
Manifest:https://uipkge.dev/r/angular/privacy-data-export-portal.jsonInstalls to:src/app/components/blocks/privacy-data-export-portal/

Variants

Loading interactive previews…

Schema

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

DataCategory
interface DataCategory {
  id: string
  name: string
  description: string
  sizeMb: number
  recordCount: string
  icon: unknown
}
ArchiveRecord
interface ArchiveRecord {
  id: string
  name: string
  format: ExportFormat
  requestedAt: string
  expiresAt: string
  expiresInDays: number
  sizeMb: number
  categories: string[]
  status: ArchiveStatus
}

Files installed (2)

  • src/app/components/blocks/privacy-data-export-portal/privacy-data-export-portal.component.ts26.8 kB
    // GDPR Article 15 / CCPA personal data export portal: governance stat cards, granular
    // category selection, format chooser, encrypted archive generation, and archive history.
    // Port of the React block 1:1.
    import { ChangeDetectionStrategy, Component, Input, computed, signal } from '@angular/core'
    import {
      Activity,
      Archive,
      Calendar,
      Check,
      CheckCircle2,
      Clock,
      CreditCard,
      Download,
      FileSpreadsheet,
      FileText,
      FolderArchive,
      HardDrive,
      Info,
      Loader2,
      Lock,
      LucideAngularModule,
      RefreshCw,
      ShieldCheck,
      User,
    } 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 { UiCheckboxComponent } from '@/ui/checkbox/checkbox.component'
    import {
      UiSelectComponent,
      UiSelectContentComponent,
      UiSelectItemComponent,
      UiSelectTriggerComponent,
      UiSelectValueComponent,
    } from '@/ui/select/select.component'
    import { UiSeparatorComponent } from '@/ui/separator/separator.component'
    import {
      UiTableBodyComponent,
      UiTableCellComponent,
      UiTableComponent,
      UiTableHeadComponent,
      UiTableHeaderComponent,
      UiTableRowComponent,
    } from '@/ui/table/table.component'
    
    export type ExportFormat = 'zip' | 'json' | 'csv'
    export type ArchiveStatus = 'ready' | 'processing' | 'expired'
    
    export interface DataCategory {
      id: string
      name: string
      description: string
      sizeMb: number
      recordCount: string
      icon: unknown
    }
    
    export interface ArchiveRecord {
      id: string
      name: string
      format: ExportFormat
      requestedAt: string
      expiresAt: string
      expiresInDays: number
      sizeMb: number
      categories: string[]
      status: ArchiveStatus
    }
    
    const CATEGORIES: DataCategory[] = [
      {
        id: 'profile',
        name: 'Profile & Identity',
        description: 'Account details, email history, avatar, and authentication security credentials.',
        sizeMb: 1.4,
        recordCount: '12 records',
        icon: User,
      },
      {
        id: 'activity',
        name: 'Activity & Audit Logs',
        description: 'Login history, device sessions, security events, and API access traces.',
        sizeMb: 8.6,
        recordCount: '1,420 events',
        icon: Activity,
      },
      {
        id: 'financial',
        name: 'Financial & Transactions',
        description: 'Invoices, payment receipts, subscription history, and billing ledger entries.',
        sizeMb: 3.2,
        recordCount: '48 entries',
        icon: CreditCard,
      },
      {
        id: 'workspace',
        name: 'Workspace Content & Projects',
        description: 'Created blocks, saved templates, comments, custom presets, and file uploads.',
        sizeMb: 35.0,
        recordCount: '312 objects',
        icon: FolderArchive,
      },
    ]
    
    const INITIAL_ARCHIVES: ArchiveRecord[] = [
      {
        id: 'DPA-2026-0814',
        name: 'DPA-2026-0814.zip',
        format: 'zip',
        requestedAt: 'Aug 14, 2026 · 09:24 UTC',
        expiresAt: 'Aug 25, 2026',
        expiresInDays: 4,
        sizeMb: 42.1,
        categories: ['Profile', 'Logs', 'Workspace', 'Billing'],
        status: 'ready',
      },
      {
        id: 'DPA-2026-0820',
        name: 'DPA-2026-0820.json',
        format: 'json',
        requestedAt: 'Aug 20, 2026 · 14:15 UTC',
        expiresAt: 'Aug 27, 2026',
        expiresInDays: 7,
        sizeMb: 8.6,
        categories: ['Activity & Audit Logs'],
        status: 'processing',
      },
      {
        id: 'DPA-2026-0201',
        name: 'DPA-2026-0201.zip',
        format: 'zip',
        requestedAt: 'Feb 01, 2026 · 11:02 UTC',
        expiresAt: 'Feb 08, 2026',
        expiresInDays: 0,
        sizeMb: 38.4,
        categories: ['Full Account Archive'],
        status: 'expired',
      },
    ]
    
    @Component({
      changeDetection: ChangeDetectionStrategy.Eager,
      selector: 'ui-privacy-data-export-portal, [ui-privacy-data-export-portal]',
      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,
        UiCardHeaderComponent,
        UiCardTitleComponent,
        UiCheckboxComponent,
        UiSelectComponent,
        UiSelectContentComponent,
        UiSelectItemComponent,
        UiSelectTriggerComponent,
        UiSelectValueComponent,
        UiSeparatorComponent,
        UiTableBodyComponent,
        UiTableCellComponent,
        UiTableComponent,
        UiTableHeadComponent,
        UiTableHeaderComponent,
        UiTableRowComponent,
      ],
      template: `
        <div data-slot="privacy-data-export-portal" [class]="rootClass">
          <!-- Header Bar -->
          <div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
            <div class="space-y-1">
              <div class="flex items-center gap-2">
                <h1 class="text-foreground text-2xl font-bold tracking-tight sm:text-3xl">
                  Data Privacy & Personal Data Export
                </h1>
                <span
                  ui-badge
                  variant="outline"
                  class="border-primary/20 bg-primary/5 text-primary hidden items-center gap-1 text-xs font-medium sm:inline-flex"
                >
                  <lucide-icon [img]="Lock" class="size-3" />
                  GDPR Art. 15
                </span>
              </div>
              <p class="text-muted-foreground text-sm">
                Request, generate, and download your complete personal data archives under GDPR Article 15.
              </p>
            </div>
            <div class="flex items-center gap-2.5">
              <button
                ui-button
                type="button"
                size="default"
                class="gap-2 shadow-xs"
                [disabled]="isGenerating()"
                (click)="requestFullExport()"
              >
                <lucide-icon [img]="Download" class="size-4" />
                Request Full Data Export
              </button>
            </div>
          </div>
    
          <!-- Notification Banner -->
          @if (notification(); as note) {
            <div [class]="notificationClass(note.type)">
              @if (note.type === 'success') {
                <lucide-icon [img]="CheckCircle2" class="text-success mt-0.5 size-5 shrink-0" />
              } @else {
                <lucide-icon [img]="Info" class="text-info mt-0.5 size-5 shrink-0" />
              }
              <div class="flex-1 space-y-0.5">
                <p class="font-medium">{{ note.title }}</p>
                <p class="text-xs opacity-90">{{ note.message }}</p>
              </div>
              <button
                ui-button
                variant="ghost"
                size="sm"
                class="h-7 px-2 text-xs hover:bg-transparent"
                (click)="notification.set(null)"
              >
                Dismiss
              </button>
            </div>
          }
    
          <!-- 4 Privacy & Governance Stat Cards -->
          <div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
            <div ui-card class="border-border shadow-xs">
              <div ui-card-header class="flex flex-row items-center justify-between space-y-0 pb-2">
                <h3 ui-card-title class="text-muted-foreground text-sm font-medium">Account Creation Date</h3>
                <div
                  class="border-border bg-muted/50 text-foreground flex size-8 items-center justify-center rounded-md border"
                >
                  <lucide-icon [img]="Calendar" class="text-muted-foreground size-4" />
                </div>
              </div>
              <div ui-card-content class="space-y-1">
                <div class="text-foreground text-xl font-bold tracking-tight tabular-nums">Nov 12, 2024</div>
                <p class="text-muted-foreground text-xs">
                  <span class="text-foreground font-medium tabular-nums">1.8 years</span> active account age
                </p>
              </div>
            </div>
    
            <div ui-card class="border-border shadow-xs">
              <div ui-card-header class="flex flex-row items-center justify-between space-y-0 pb-2">
                <h3 ui-card-title class="text-muted-foreground text-sm font-medium">Total Data Footprint</h3>
                <div
                  class="border-border bg-muted/50 text-foreground flex size-8 items-center justify-center rounded-md border"
                >
                  <lucide-icon [img]="HardDrive" class="text-muted-foreground size-4" />
                </div>
              </div>
              <div ui-card-content class="space-y-1">
                <div class="text-foreground text-xl font-bold tracking-tight tabular-nums">48.2 MB</div>
                <p class="text-muted-foreground text-xs">Across 6 storage domains</p>
              </div>
            </div>
    
            <div ui-card class="border-border shadow-xs">
              <div ui-card-header class="flex flex-row items-center justify-between space-y-0 pb-2">
                <h3 ui-card-title class="text-muted-foreground text-sm font-medium">Data Retention Policy</h3>
                <div
                  class="border-border bg-muted/50 text-foreground flex size-8 items-center justify-center rounded-md border"
                >
                  <lucide-icon [img]="Clock" class="text-muted-foreground size-4" />
                </div>
              </div>
              <div ui-card-content class="space-y-1">
                <div class="text-foreground text-xl font-bold tracking-tight tabular-nums">30-Day Policy</div>
                <p class="text-muted-foreground text-xs">Automated purge of activity logs</p>
              </div>
            </div>
    
            <div ui-card class="border-border shadow-xs">
              <div ui-card-header class="flex flex-row items-center justify-between space-y-0 pb-2">
                <h3 ui-card-title class="text-muted-foreground text-sm font-medium">Consent Status</h3>
                <div
                  class="border-success/20 bg-success/10 text-success flex size-8 items-center justify-center rounded-md border"
                >
                  <lucide-icon [img]="ShieldCheck" class="size-4" />
                </div>
              </div>
              <div ui-card-content class="space-y-1">
                <div class="text-foreground text-xl font-bold tracking-tight tabular-nums">3 / 3 Active</div>
                <p class="text-success text-xs font-medium">Required Consents Active</p>
              </div>
            </div>
          </div>
    
          <!-- Select Data Categories for Export -->
          <div ui-card class="border-border shadow-xs">
            <div ui-card-header class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
              <div class="space-y-1">
                <h3 ui-card-title class="text-foreground text-lg font-semibold">Select Data Categories for Export</h3>
                <p ui-card-description>Choose specific data domains to compile into your encrypted archive package.</p>
              </div>
              <button
                ui-button
                variant="outline"
                size="sm"
                class="h-8 self-start text-xs sm:self-auto"
                (click)="toggleAll()"
              >
                {{ isAllSelected() ? 'Deselect All' : 'Select All Categories' }}
              </button>
            </div>
            <div ui-card-content class="space-y-6">
              <div class="grid grid-cols-1 gap-3.5 md:grid-cols-2">
                @for (cat of categories; track cat.id) {
                  <div [class]="categoryCardClass(cat.id)" (click)="toggleCategory(cat.id)">
                    <ui-checkbox
                      [id]="'category-' + cat.id"
                      [checked]="selectedCategories().includes(cat.id)"
                      class="mt-1"
                      (checkedChange)="toggleCategory(cat.id)"
                      (click)="$event.stopPropagation()"
                    />
                    <div class="min-w-0 flex-1 space-y-1.5">
                      <div class="flex items-center justify-between gap-2">
                        <div class="flex items-center gap-2">
                          <lucide-icon [img]="$any(cat.icon)" class="text-muted-foreground size-4" />
                          <label
                            [for]="'category-' + cat.id"
                            class="text-foreground cursor-pointer text-sm leading-none font-medium"
                            (click)="$event.stopPropagation(); toggleCategory(cat.id)"
                          >
                            {{ cat.name }}
                          </label>
                        </div>
                        <span ui-badge variant="secondary" class="shrink-0 font-mono text-xs tabular-nums">
                          {{ cat.sizeMb }} MB
                        </span>
                      </div>
                      <p class="text-muted-foreground text-xs leading-relaxed">{{ cat.description }}</p>
                      <div class="text-muted-foreground/80 text-xs tabular-nums">
                        Estimated footprint: <span class="text-foreground font-medium">{{ cat.recordCount }}</span>
                      </div>
                    </div>
                  </div>
                }
              </div>
    
              <ui-separator />
    
              <div class="flex flex-col gap-4 sm:flex-row sm:flex-wrap sm:items-center sm:justify-between">
                <div class="flex flex-col gap-1.5 sm:flex-row sm:items-center sm:gap-3">
                  <label class="text-foreground text-sm font-medium whitespace-nowrap">Export Format:</label>
                  <ui-select [value]="selectedFormat()" (valueChange)="setFormat($event)">
                    <button ui-select-trigger class="bg-background w-full sm:w-[260px]">
                      <ui-select-value placeholder="Select export format" />
                    </button>
                    <ui-select-content>
                      <ui-select-item value="zip">
                        <span class="flex items-center gap-2">
                          <lucide-icon [img]="Archive" class="text-muted-foreground size-3.5" />
                          <span>Complete ZIP Bundle (.zip)</span>
                        </span>
                      </ui-select-item>
                      <ui-select-item value="json">
                        <span class="flex items-center gap-2">
                          <lucide-icon [img]="FileText" class="text-muted-foreground size-3.5" />
                          <span>JSON Archive (.json)</span>
                        </span>
                      </ui-select-item>
                      <ui-select-item value="csv">
                        <span class="flex items-center gap-2">
                          <lucide-icon [img]="FileSpreadsheet" class="text-muted-foreground size-3.5" />
                          <span>CSV Spreadsheet (.csv)</span>
                        </span>
                      </ui-select-item>
                    </ui-select-content>
                  </ui-select>
                </div>
    
                <div class="flex flex-col items-stretch gap-3 sm:flex-row sm:items-center">
                  <div class="text-muted-foreground text-xs sm:text-right">
                    Selected:
                    <span class="text-foreground font-semibold tabular-nums">
                      {{ selectedCategories().length }} of {{ categories.length }} categories
                    </span>
                    <span class="text-foreground font-semibold tabular-nums">~{{ totalSelectedSize() }} MB</span>
                  </div>
                  <button
                    ui-button
                    type="button"
                    class="gap-2 shadow-xs"
                    [disabled]="selectedCategories().length === 0 || isGenerating()"
                    (click)="triggerExport('Custom Data Export')"
                  >
                    @if (isGenerating()) {
                      <lucide-icon [img]="Loader2" class="size-4 animate-spin" />
                    } @else {
                      <lucide-icon [img]="Archive" class="size-4" />
                    }
                    {{ isGenerating() ? 'Compiling Archive...' : 'Generate Data Archive' }}
                  </button>
                </div>
              </div>
            </div>
          </div>
    
          <!-- Legal Compliance Notice Box -->
          <div
            class="border-border bg-muted/40 text-muted-foreground flex items-start gap-3 rounded-lg border p-4 text-xs shadow-xs"
          >
            <lucide-icon [img]="Lock" class="text-muted-foreground mt-0.5 size-4 shrink-0" />
            <p class="leading-relaxed">
              Under <span class="text-foreground font-medium">GDPR Article 15</span> (Right of Access) and
              <span class="text-foreground font-medium">CCPA §1798.100</span>, you are entitled to request and receive all
              personal data processed by this service. Export packages are generated on-demand, encrypted with AES-256, and
              available for download for 7 days before automated deletion.
            </p>
          </div>
    
          <!-- Export Archive History Table -->
          <div ui-card class="border-border shadow-xs">
            <div ui-card-header>
              <div class="flex items-center justify-between">
                <div class="space-y-1">
                  <h3 ui-card-title class="text-foreground text-lg font-semibold">Export Archive History</h3>
                  <p ui-card-description>
                    Previous data archive requests and generation status. Downloads remain available for 7 days.
                  </p>
                </div>
                <span ui-badge variant="outline" class="font-mono text-xs tabular-nums">
                  {{ archives().length }} archives
                </span>
              </div>
            </div>
            <div ui-card-content class="p-0">
              <div class="overflow-x-auto">
                <table ui-table>
                  <thead ui-table-header>
                    <tr ui-table-row>
                      <th ui-table-head class="min-w-[220px]">Archive ID & Format</th>
                      <th ui-table-head class="min-w-[170px]">Request Date</th>
                      <th ui-table-head class="min-w-[140px]">Expiration</th>
                      <th ui-table-head class="min-w-[100px] text-right">File Size</th>
                      <th ui-table-head class="min-w-[150px]">Status</th>
                      <th ui-table-head class="min-w-[140px] text-right">Action</th>
                    </tr>
                  </thead>
                  <tbody ui-table-body>
                    @for (arc of archives(); track arc.id) {
                      <tr ui-table-row class="hover:bg-muted/40">
                        <td ui-table-cell>
                          <div class="space-y-1">
                            <div class="flex items-center gap-2">
                              <span class="text-foreground font-mono text-xs font-semibold">{{ arc.name }}</span>
                              <span ui-badge variant="outline" class="px-1.5 py-0 font-mono text-xs uppercase">
                                {{ arc.format }}
                              </span>
                            </div>
                            <p class="text-muted-foreground text-xs">{{ arc.categories.join(', ') }}</p>
                          </div>
                        </td>
                        <td ui-table-cell class="text-muted-foreground text-xs tabular-nums">{{ arc.requestedAt }}</td>
                        <td ui-table-cell>
                          <div class="space-y-0.5">
                            <span [class]="expiryClass(arc)">
                              {{ arc.status === 'expired' ? 'Expired' : 'Expires in ' + arc.expiresInDays + ' days' }}
                            </span>
                            <p class="text-muted-foreground/70 text-xs tabular-nums">{{ arc.expiresAt }}</p>
                          </div>
                        </td>
                        <td ui-table-cell class="text-foreground text-right font-mono text-xs font-medium tabular-nums">
                          {{ arc.sizeMb.toFixed(1) }} MB
                        </td>
                        <td ui-table-cell>
                          @if (arc.status === 'ready') {
                            <span
                              ui-badge
                              variant="outline"
                              class="border-success/30 bg-success/10 text-success items-center gap-1.5 text-xs font-medium"
                            >
                              <lucide-icon [img]="Check" class="size-3" />
                              Ready for Download
                            </span>
                          } @else if (arc.status === 'processing') {
                            <span
                              ui-badge
                              variant="outline"
                              class="border-warning/30 bg-warning/10 text-warning items-center gap-1.5 text-xs font-medium"
                            >
                              <lucide-icon [img]="Loader2" class="size-3 animate-spin" />
                              Processing Archive
                            </span>
                          } @else {
                            <span ui-badge variant="secondary" class="text-muted-foreground text-xs">Expired</span>
                          }
                        </td>
                        <td ui-table-cell class="text-right">
                          @if (arc.status === 'ready') {
                            <button
                              ui-button
                              aria-label="Download attachment"
                              size="sm"
                              class="h-8 gap-1.5 text-xs shadow-xs"
                              (click)="handleDownload(arc)"
                            >
                              <lucide-icon [img]="Download" class="size-3.5" />
                              Download ZIP
                            </button>
                          } @else if (arc.status === 'processing') {
                            <button ui-button size="sm" variant="secondary" disabled class="h-8 gap-1.5 text-xs opacity-75">
                              <lucide-icon [img]="Loader2" class="size-3.5 animate-spin" />
                              Processing...
                            </button>
                          } @else {
                            <button
                              ui-button
                              size="sm"
                              variant="outline"
                              class="h-8 gap-1.5 text-xs"
                              (click)="handleReRequest(arc)"
                            >
                              <lucide-icon [img]="RefreshCw" class="size-3.5" />
                              Re-request
                            </button>
                          }
                        </td>
                      </tr>
                    }
                  </tbody>
                </table>
              </div>
            </div>
          </div>
        </div>
      `,
    })
    export class UiPrivacyDataExportPortalComponent {
      protected readonly Activity = Activity
      protected readonly Archive = Archive
      protected readonly Calendar = Calendar
      protected readonly Check = Check
      protected readonly CheckCircle2 = CheckCircle2
      protected readonly Clock = Clock
      protected readonly CreditCard = CreditCard
      protected readonly Download = Download
      protected readonly FileSpreadsheet = FileSpreadsheet
      protected readonly FileText = FileText
      protected readonly FolderArchive = FolderArchive
      protected readonly HardDrive = HardDrive
      protected readonly Info = Info
      protected readonly Loader2 = Loader2
      protected readonly Lock = Lock
      protected readonly RefreshCw = RefreshCw
      protected readonly ShieldCheck = ShieldCheck
      protected readonly User = User
    
      protected readonly categories = CATEGORIES
    
      @Input() set initialCategories(v: string[] | undefined) {
        this.selectedCategories.set([...(v ?? CATEGORIES.map((c) => c.id))])
      }
      @Input() set initialFormat(v: ExportFormat | undefined) {
        this.selectedFormat.set(v ?? 'zip')
      }
      @Input('class') className?: string
    
      readonly selectedCategories = signal<string[]>(CATEGORIES.map((c) => c.id))
      readonly selectedFormat = signal<ExportFormat>('zip')
      readonly archives = signal<ArchiveRecord[]>(INITIAL_ARCHIVES.map((a) => ({ ...a, categories: [...a.categories] })))
      readonly isGenerating = signal(false)
      readonly notification = signal<{ title: string; message: string; type: 'success' | 'info' } | null>(null)
    
      readonly totalSelectedSize = computed(() => {
        const sum = CATEGORIES.filter((c) => this.selectedCategories().includes(c.id)).reduce((acc, c) => acc + c.sizeMb, 0)
        return sum.toFixed(1)
      })
    
      readonly isAllSelected = computed(() => this.selectedCategories().length === CATEGORIES.length)
    
      get rootClass(): string {
        return cn('w-full space-y-6', this.className)
      }
    
      notificationClass(type: 'success' | 'info'): string {
        return cn(
          'flex items-start gap-3 rounded-lg border p-4 text-sm transition-colors',
          type === 'success' ? 'border-success/30 bg-success/10 text-success' : 'border-info/30 bg-info/10 text-info',
        )
      }
    
      categoryCardClass(categoryId: string): string {
        return cn(
          'group flex cursor-pointer items-start gap-3.5 rounded-lg border p-4 transition-colors',
          this.selectedCategories().includes(categoryId)
            ? 'border-primary/50 bg-primary/5 dark:bg-primary/10 shadow-xs'
            : 'border-border bg-card hover:bg-muted/40',
        )
      }
    
      expiryClass(arc: ArchiveRecord): string {
        return cn(
          'text-xs font-medium tabular-nums',
          arc.status === 'expired'
            ? 'text-muted-foreground line-through'
            : arc.expiresInDays <= 4
              ? 'text-warning'
              : 'text-muted-foreground',
        )
      }
    
      setFormat(value: string): void {
        this.selectedFormat.set(value as ExportFormat)
      }
    
      toggleCategory(categoryId: string): void {
        this.selectedCategories.update((prev) =>
          prev.includes(categoryId) ? prev.filter((id) => id !== categoryId) : [...prev, categoryId],
        )
      }
    
      toggleAll(): void {
        this.selectedCategories.set(this.isAllSelected() ? [] : CATEGORIES.map((c) => c.id))
      }
    
      requestFullExport(): void {
        this.selectedCategories.set(CATEGORIES.map((c) => c.id))
        this.selectedFormat.set('zip')
        this.triggerExport('Full Data Export')
      }
    
      triggerExport(label = 'Custom Data Export'): void {
        if (this.selectedCategories().length === 0) return
    
        this.isGenerating.set(true)
        this.notification.set({
          title: 'Archive Request Queued',
          message: `${label} requested. Compiling ${this.selectedCategories().length} categories (${this.totalSelectedSize()} MB) with AES-256 encryption.`,
          type: 'info',
        })
    
        setTimeout(() => {
          const newId = `DPA-${new Date().toISOString().slice(0, 10).replace(/-/g, '')}-${Math.floor(100 + Math.random() * 900)}`
          const ext = this.selectedFormat()
          const catNames = CATEGORIES.filter((c) => this.selectedCategories().includes(c.id)).map(
            (c) => c.name.split(' ')[0],
          )
    
          const newRecord: ArchiveRecord = {
            id: newId,
            name: `${newId}.${ext}`,
            format: this.selectedFormat(),
            requestedAt: 'Just now',
            expiresAt: '7 days from now',
            expiresInDays: 7,
            sizeMb: parseFloat(this.totalSelectedSize()),
            categories: catNames,
            status: 'ready',
          }
    
          this.archives.update((prev) => [newRecord, ...prev])
          this.isGenerating.set(false)
          this.notification.set({
            title: 'Archive Ready',
            message: `Export package ${newRecord.name} (${newRecord.sizeMb} MB) has been generated and is ready for download.`,
            type: 'success',
          })
        }, 1200)
      }
    
      handleDownload(archive: ArchiveRecord): void {
        this.notification.set({
          title: 'Download Initiated',
          message: `Securely downloading ${archive.name} (${archive.sizeMb} MB). SHA-256 checksum verified.`,
          type: 'success',
        })
      }
    
      handleReRequest(archive: ArchiveRecord): void {
        this.notification.set({
          title: 'Archive Re-generation Requested',
          message: `Re-generating expired archive for ${archive.categories.join(', ')}.`,
          type: 'info',
        })
      }
    }
    
  • src/app/components/blocks/privacy-data-export-portal/index.ts0.2 kB

Raw manifest:https://uipkge.dev/r/angular/privacy-data-export-portal.json