Privacy Data Export Portal
blocklegalGDPR 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
$pnpm dlx uipkge-ng@latest add privacy-data-export-portal$npx uipkge-ng@latest add privacy-data-export-portal$yarn dlx uipkge-ng@latest add privacy-data-export-portal$bunx uipkge-ng@latest add privacy-data-export-portalManifest:
https://uipkge.dev/r/angular/privacy-data-export-portal.jsonInstalls to:src/app/components/blocks/privacy-data-export-portal/Variants
Loading interactive previews…
Loading interactive previews…
Schema
Type aliases exported from this item's source. Use these to shape the data you pass in.
DataCategoryinterface DataCategory {
id: string
name: string
description: string
sizeMb: number
recordCount: string
icon: unknown
}ArchiveRecordinterface 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
export { UiPrivacyDataExportPortalComponent, type ExportFormat, type ArchiveStatus, type DataCategory, type ArchiveRecord, } from './privacy-data-export-portal.component'
Raw manifest:https://uipkge.dev/r/angular/privacy-data-export-portal.json