UIPackage
Menu

Framework

Change language

Boilerplate 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.

Also available for Vue ->

Installation

$npx shadcn@latest add https://uipkge.dev/r/react/privacy-data-export-portal.json
Named registry:npx shadcn@latest add @uipkge-react/privacy-data-export-portalInstalls to:components/blocks/

Variants

Loading interactive previews…

Props

NameType / ValuesDefaultRequired
initialCategoriesstring[]optional
initialFormatExportFormatoptional
classNamestringoptional

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: React.ComponentType<{ className?: string }>
}
ArchiveRecord
interface ArchiveRecord {
  id: string
  name: string
  format: ExportFormat
  requestedAt: string
  expiresAt: string
  expiresInDays: number
  sizeMb: number
  categories: string[]
  status: ArchiveStatus
}

Files installed (1)

  • components/blocks/PrivacyDataExportPortal.tsx24.8 kB
    'use client'
    
    import * as React from 'react'
    import {
      Activity,
      Archive,
      Calendar,
      Check,
      CheckCircle2,
      Clock,
      CreditCard,
      Download,
      FileSpreadsheet,
      FileText,
      FolderArchive,
      HardDrive,
      Info,
      Loader2,
      Lock,
      RefreshCw,
      ShieldCheck,
      User,
    } from 'lucide-react'
    import { cn } from '@/lib/utils'
    import { Badge } from '@/components/ui/badge'
    import { Button } from '@/components/ui/button'
    import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
    import { Checkbox } from '@/components/ui/checkbox'
    import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
    import { Separator } from '@/components/ui/separator'
    import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
    
    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: React.ComponentType<{ className?: string }>
    }
    
    export interface ArchiveRecord {
      id: string
      name: string
      format: ExportFormat
      requestedAt: string
      expiresAt: string
      expiresInDays: number
      sizeMb: number
      categories: string[]
      status: ArchiveStatus
    }
    
    export interface PrivacyDataExportPortalProps {
      initialCategories?: string[]
      initialFormat?: ExportFormat
      className?: string
    }
    
    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 initialArchives: 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',
      },
    ]
    
    export function PrivacyDataExportPortal({
      initialCategories = ['profile', 'activity', 'financial', 'workspace'],
      initialFormat = 'zip',
      className,
    }: PrivacyDataExportPortalProps) {
      const [selectedCategories, setSelectedCategories] = React.useState<string[]>(initialCategories)
      const [selectedFormat, setSelectedFormat] = React.useState<ExportFormat>(initialFormat)
      const [archives, setArchives] = React.useState<ArchiveRecord[]>(initialArchives)
      const [isGenerating, setIsGenerating] = React.useState(false)
      const [notification, setNotification] = React.useState<{
        title: string
        message: string
        type: 'success' | 'info'
      } | null>(null)
    
      const totalSelectedSize = React.useMemo(() => {
        const sum = categories.filter((c) => selectedCategories.includes(c.id)).reduce((acc, c) => acc + c.sizeMb, 0)
        return sum.toFixed(1)
      }, [selectedCategories])
    
      const isAllSelected = selectedCategories.length === categories.length
    
      const toggleCategory = (categoryId: string) => {
        setSelectedCategories((prev) =>
          prev.includes(categoryId) ? prev.filter((id) => id !== categoryId) : [...prev, categoryId],
        )
      }
    
      const toggleAll = () => {
        if (isAllSelected) {
          setSelectedCategories([])
        } else {
          setSelectedCategories(categories.map((c) => c.id))
        }
      }
    
      const triggerExport = (label = 'Custom Data Export', formatToUse = selectedFormat) => {
        if (selectedCategories.length === 0) return
    
        setIsGenerating(true)
        setNotification({
          title: 'Archive Request Queued',
          message: `${label} requested. Compiling ${selectedCategories.length} categories (${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 catNames = categories.filter((c) => selectedCategories.includes(c.id)).map((c) => c.name.split(' ')[0])
    
          const newRecord: ArchiveRecord = {
            id: newId,
            name: `${newId}.${formatToUse}`,
            format: formatToUse,
            requestedAt: 'Just now',
            expiresAt: '7 days from now',
            expiresInDays: 7,
            sizeMb: parseFloat(totalSelectedSize),
            categories: catNames,
            status: 'ready',
          }
    
          setArchives((prev) => [newRecord, ...prev])
          setIsGenerating(false)
          setNotification({
            title: 'Archive Ready',
            message: `Export package ${newRecord.name} (${newRecord.sizeMb} MB) has been generated and is ready for download.`,
            type: 'success',
          })
        }, 1200)
      }
    
      const requestFullExport = () => {
        setSelectedCategories(categories.map((c) => c.id))
        setSelectedFormat('zip')
        triggerExport('Full Data Export', 'zip')
      }
    
      const handleDownload = (archive: ArchiveRecord) => {
        setNotification({
          title: 'Download Initiated',
          message: `Securely downloading ${archive.name} (${archive.sizeMb} MB). SHA-256 checksum verified.`,
          type: 'success',
        })
      }
    
      const handleReRequest = (archive: ArchiveRecord) => {
        setNotification({
          title: 'Archive Re-generation Requested',
          message: `Re-generating expired archive for ${archive.categories.join(', ')}.`,
          type: 'info',
        })
      }
    
      return (
        <div data-slot="privacy-data-export-portal" className={cn('w-full space-y-6', className)}>
          {/* Header Bar */}
          <div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
            <div className="space-y-1">
              <div className="flex items-center gap-2">
                <h1 className="text-foreground text-2xl font-bold tracking-tight sm:text-3xl">
                  Data Privacy & Personal Data Export
                </h1>
                <Badge
                  variant="outline"
                  className="border-primary/20 bg-primary/5 text-primary hidden items-center gap-1 text-xs font-medium sm:inline-flex"
                >
                  <Lock className="size-3" />
                  GDPR Art. 15
                </Badge>
              </div>
              <p className="text-muted-foreground text-sm">
                Request, generate, and download your complete personal data archives under GDPR Article 15.
              </p>
            </div>
            <div className="flex items-center gap-2.5">
              <Button
                type="button"
                size="default"
                className="gap-2 shadow-xs"
                disabled={isGenerating}
                onClick={requestFullExport}
              >
                <Download className="size-4" />
                Request Full Data Export
              </Button>
            </div>
          </div>
    
          {/* Notification Banner */}
          {notification && (
            <div
              className={cn(
                'flex items-start gap-3 rounded-lg border p-4 text-sm transition-colors',
                notification.type === 'success'
                  ? 'border-success/30 bg-success/10 text-success'
                  : 'border-info/30 bg-info/10 text-info',
              )}
            >
              {notification.type === 'success' ? (
                <CheckCircle2 className="text-success mt-0.5 size-5 shrink-0" />
              ) : (
                <Info className="text-info mt-0.5 size-5 shrink-0" />
              )}
              <div className="flex-1 space-y-0.5">
                <p className="font-medium">{notification.title}</p>
                <p className="text-xs opacity-90">{notification.message}</p>
              </div>
              <Button
                variant="ghost"
                size="sm"
                className="h-7 px-2 text-xs hover:bg-transparent"
                onClick={() => setNotification(null)}
              >
                Dismiss
              </Button>
            </div>
          )}
    
          {/* 4 Privacy & Governance Stat Cards */}
          <div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
            {/* Stat 1: Account Creation Date */}
            <Card className="border-border shadow-xs">
              <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
                <CardTitle className="text-muted-foreground text-sm font-medium">Account Creation Date</CardTitle>
                <div className="border-border bg-muted/50 text-foreground flex size-8 items-center justify-center rounded-md border">
                  <Calendar className="text-muted-foreground size-4" />
                </div>
              </CardHeader>
              <CardContent className="space-y-1">
                <div className="text-foreground text-xl font-bold tracking-tight tabular-nums">Nov 12, 2024</div>
                <p className="text-muted-foreground text-xs">
                  <span className="text-foreground font-medium tabular-nums">1.8 years</span> active account age
                </p>
              </CardContent>
            </Card>
    
            {/* Stat 2: Total Data Footprint */}
            <Card className="border-border shadow-xs">
              <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
                <CardTitle className="text-muted-foreground text-sm font-medium">Total Data Footprint</CardTitle>
                <div className="border-border bg-muted/50 text-foreground flex size-8 items-center justify-center rounded-md border">
                  <HardDrive className="text-muted-foreground size-4" />
                </div>
              </CardHeader>
              <CardContent className="space-y-1">
                <div className="text-foreground text-xl font-bold tracking-tight tabular-nums">48.2 MB</div>
                <p className="text-muted-foreground text-xs">Across 6 storage domains</p>
              </CardContent>
            </Card>
    
            {/* Stat 3: Active Data Retention Policy */}
            <Card className="border-border shadow-xs">
              <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
                <CardTitle className="text-muted-foreground text-sm font-medium">Data Retention Policy</CardTitle>
                <div className="border-border bg-muted/50 text-foreground flex size-8 items-center justify-center rounded-md border">
                  <Clock className="text-muted-foreground size-4" />
                </div>
              </CardHeader>
              <CardContent className="space-y-1">
                <div className="text-foreground text-xl font-bold tracking-tight tabular-nums">30-Day Policy</div>
                <p className="text-muted-foreground text-xs">Automated purge of activity logs</p>
              </CardContent>
            </Card>
    
            {/* Stat 4: Consent Status */}
            <Card className="border-border shadow-xs">
              <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
                <CardTitle className="text-muted-foreground text-sm font-medium">Consent Status</CardTitle>
                <div className="border-success/20 bg-success/10 text-success flex size-8 items-center justify-center rounded-md border">
                  <ShieldCheck className="size-4" />
                </div>
              </CardHeader>
              <CardContent className="space-y-1">
                <div className="text-foreground text-xl font-bold tracking-tight tabular-nums">3 / 3 Active</div>
                <p className="text-success text-xs font-medium">Required Consents Active</p>
              </CardContent>
            </Card>
          </div>
    
          {/* Select Data Categories for Export */}
          <Card className="border-border shadow-xs">
            <CardHeader className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
              <div className="space-y-1">
                <CardTitle className="text-foreground text-lg font-semibold">Select Data Categories for Export</CardTitle>
                <CardDescription>
                  Choose specific data domains to compile into your encrypted archive package.
                </CardDescription>
              </div>
              <Button variant="outline" size="sm" className="h-8 self-start text-xs sm:self-auto" onClick={toggleAll}>
                {isAllSelected ? 'Deselect All' : 'Select All Categories'}
              </Button>
            </CardHeader>
            <CardContent className="space-y-6">
              {/* Checkbox Grid of 4 Categories */}
              <div className="grid grid-cols-1 gap-3.5 md:grid-cols-2">
                {categories.map((cat) => {
                  const Icon = cat.icon
                  const isChecked = selectedCategories.includes(cat.id)
                  return (
                    <div
                      key={cat.id}
                      className={cn(
                        'group flex cursor-pointer items-start gap-3.5 rounded-lg border p-4 transition-colors',
                        isChecked
                          ? 'border-primary/50 bg-primary/5 dark:bg-primary/10 shadow-xs'
                          : 'border-border bg-card hover:bg-muted/40',
                      )}
                      onClick={() => toggleCategory(cat.id)}
                    >
                      <Checkbox
                        id={`category-${cat.id}`}
                        checked={isChecked}
                        className="mt-1"
                        onCheckedChange={() => toggleCategory(cat.id)}
                        onClick={(e) => e.stopPropagation()}
                      />
                      <div className="min-w-0 flex-1 space-y-1.5">
                        <div className="flex items-center justify-between gap-2">
                          <div className="flex items-center gap-2">
                            <Icon className="text-muted-foreground size-4" />
                            <label
                              htmlFor={`category-${cat.id}`}
                              className="text-foreground cursor-pointer text-sm leading-none font-medium"
                              onClick={(e) => {
                                e.stopPropagation()
                                toggleCategory(cat.id)
                              }}
                            >
                              {cat.name}
                            </label>
                          </div>
                          <Badge variant="secondary" className="shrink-0 font-mono text-xs tabular-nums">
                            {cat.sizeMb} MB
                          </Badge>
                        </div>
                        <p className="text-muted-foreground text-xs leading-relaxed">{cat.description}</p>
                        <div className="text-muted-foreground/80 text-xs tabular-nums">
                          Estimated footprint: <span className="text-foreground font-medium">{cat.recordCount}</span>
                        </div>
                      </div>
                    </div>
                  )
                })}
              </div>
    
              <Separator />
    
              {/* Format selector and generation action bar */}
              <div className="flex flex-col gap-4 sm:flex-row sm:flex-wrap sm:items-center sm:justify-between">
                <div className="flex flex-col gap-1.5 sm:flex-row sm:items-center sm:gap-3">
                  <label className="text-foreground text-sm font-medium whitespace-nowrap">Export Format:</label>
                  <Select value={selectedFormat} onValueChange={(val: ExportFormat) => setSelectedFormat(val)}>
                    <SelectTrigger className="bg-background w-full sm:w-[260px]">
                      <SelectValue placeholder="Select export format" />
                    </SelectTrigger>
                    <SelectContent>
                      <SelectItem value="zip">
                        <div className="flex items-center gap-2">
                          <Archive className="text-muted-foreground size-3.5" />
                          <span>Complete ZIP Bundle (.zip)</span>
                        </div>
                      </SelectItem>
                      <SelectItem value="json">
                        <div className="flex items-center gap-2">
                          <FileText className="text-muted-foreground size-3.5" />
                          <span>JSON Archive (.json)</span>
                        </div>
                      </SelectItem>
                      <SelectItem value="csv">
                        <div className="flex items-center gap-2">
                          <FileSpreadsheet className="text-muted-foreground size-3.5" />
                          <span>CSV Spreadsheet (.csv)</span>
                        </div>
                      </SelectItem>
                    </SelectContent>
                  </Select>
                </div>
    
                <div className="flex flex-col items-stretch gap-3 sm:flex-row sm:items-center">
                  <div className="text-muted-foreground text-xs sm:text-right">
                    Selected:{' '}
                    <span className="text-foreground font-semibold tabular-nums">
                      {selectedCategories.length} of {categories.length} categories
                    </span>
                    <span className="text-foreground font-semibold tabular-nums">~{totalSelectedSize} MB</span>
                  </div>
                  <Button
                    type="button"
                    className="gap-2 shadow-xs"
                    disabled={selectedCategories.length === 0 || isGenerating}
                    onClick={() => triggerExport('Custom Data Export')}
                  >
                    {isGenerating ? <Loader2 className="size-4 animate-spin" /> : <Archive className="size-4" />}
                    {isGenerating ? 'Compiling Archive...' : 'Generate Data Archive'}
                  </Button>
                </div>
              </div>
            </CardContent>
          </Card>
    
          {/* Legal Compliance Notice Box */}
          <div className="border-border bg-muted/40 text-muted-foreground flex items-start gap-3 rounded-lg border p-4 text-xs shadow-xs">
            <Lock className="text-muted-foreground mt-0.5 size-4 shrink-0" />
            <p className="leading-relaxed">
              Under <span className="text-foreground font-medium">GDPR Article 15</span> (Right of Access) and{' '}
              <span className="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 */}
          <Card className="border-border shadow-xs">
            <CardHeader>
              <div className="flex items-center justify-between">
                <div className="space-y-1">
                  <CardTitle className="text-foreground text-lg font-semibold">Export Archive History</CardTitle>
                  <CardDescription>
                    Previous data archive requests and generation status. Downloads remain available for 7 days.
                  </CardDescription>
                </div>
                <Badge variant="outline" className="font-mono text-xs tabular-nums">
                  {archives.length} archives
                </Badge>
              </div>
            </CardHeader>
            <CardContent className="p-0">
              <div className="overflow-x-auto">
                <Table>
                  <TableHeader>
                    <TableRow>
                      <TableHead className="min-w-[220px]">Archive ID & Format</TableHead>
                      <TableHead className="min-w-[170px]">Request Date</TableHead>
                      <TableHead className="min-w-[140px]">Expiration</TableHead>
                      <TableHead className="min-w-[100px] text-right">File Size</TableHead>
                      <TableHead className="min-w-[150px]">Status</TableHead>
                      <TableHead className="min-w-[140px] text-right">Action</TableHead>
                    </TableRow>
                  </TableHeader>
                  <TableBody>
                    {archives.map((arc) => (
                      <TableRow key={arc.id} className="hover:bg-muted/40">
                        <TableCell>
                          <div className="space-y-1">
                            <div className="flex items-center gap-2">
                              <span className="text-foreground font-mono text-xs font-semibold">{arc.name}</span>
                              <Badge variant="outline" className="px-1.5 py-0 font-mono text-xs uppercase">
                                {arc.format}
                              </Badge>
                            </div>
                            <p className="text-muted-foreground text-xs">{arc.categories.join(', ')}</p>
                          </div>
                        </TableCell>
                        <TableCell className="text-muted-foreground text-xs tabular-nums">{arc.requestedAt}</TableCell>
                        <TableCell>
                          <div className="space-y-0.5">
                            <span
                              className={cn(
                                'text-xs font-medium tabular-nums',
                                arc.status === 'expired'
                                  ? 'text-muted-foreground line-through'
                                  : arc.expiresInDays <= 4
                                    ? 'text-warning'
                                    : 'text-muted-foreground',
                              )}
                            >
                              {arc.status === 'expired' ? 'Expired' : `Expires in ${arc.expiresInDays} days`}
                            </span>
                            <p className="text-muted-foreground/70 text-xs tabular-nums">{arc.expiresAt}</p>
                          </div>
                        </TableCell>
                        <TableCell className="text-foreground text-right font-mono text-xs font-medium tabular-nums">
                          {arc.sizeMb.toFixed(1)} MB
                        </TableCell>
                        <TableCell>
                          {arc.status === 'ready' && (
                            <Badge
                              variant="outline"
                              className="border-success/30 bg-success/10 text-success items-center gap-1.5 text-xs font-medium"
                            >
                              <Check className="size-3" />
                              Ready for Download
                            </Badge>
                          )}
                          {arc.status === 'processing' && (
                            <Badge
                              variant="outline"
                              className="border-warning/30 bg-warning/10 text-warning items-center gap-1.5 text-xs font-medium"
                            >
                              <Loader2 className="size-3 animate-spin" />
                              Processing Archive
                            </Badge>
                          )}
                          {arc.status === 'expired' && (
                            <Badge variant="secondary" className="text-muted-foreground text-xs">
                              Expired
                            </Badge>
                          )}
                        </TableCell>
                        <TableCell className="text-right">
                          {arc.status === 'ready' && (
                            <Button
                              aria-label="Download attachment"
                              size="sm"
                              className="h-8 gap-1.5 text-xs shadow-xs"
                              onClick={() => handleDownload(arc)}
                            >
                              <Download className="size-3.5" />
                              Download ZIP
                            </Button>
                          )}
                          {arc.status === 'processing' && (
                            <Button size="sm" variant="secondary" disabled className="h-8 gap-1.5 text-xs opacity-75">
                              <Loader2 className="size-3.5 animate-spin" />
                              Processing...
                            </Button>
                          )}
                          {arc.status === 'expired' && (
                            <Button
                              size="sm"
                              variant="outline"
                              className="h-8 gap-1.5 text-xs"
                              onClick={() => handleReRequest(arc)}
                            >
                              <RefreshCw className="size-3.5" />
                              Re-request
                            </Button>
                          )}
                        </TableCell>
                      </TableRow>
                    ))}
                  </TableBody>
                </Table>
              </div>
            </CardContent>
          </Card>
        </div>
      )
    }
    

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