Settings Export Data

PreviousNext

Export your data in various formats with category selection.

Docs
hextauiui

Preview

Loading preview…
registry/new-york/blocks/settings/settings-export-data.tsx
"use client";

import { Download, FileDown, Loader2 } from "lucide-react";
import { useState } from "react";
import { cn } from "@/lib/utils";
import { Badge } from "@/registry/new-york/ui/badge";
import { Button } from "@/registry/new-york/ui/button";
import {
  Card,
  CardContent,
  CardDescription,
  CardHeader,
  CardTitle,
} from "@/registry/new-york/ui/card";
import { Checkbox } from "@/registry/new-york/ui/checkbox";
import {
  Field,
  FieldContent,
  FieldDescription,
  FieldLabel,
} from "@/registry/new-york/ui/field";
import {
  InputGroup,
  InputGroupInput,
} from "@/registry/new-york/ui/input-group";
import { Progress } from "@/registry/new-york/ui/progress";
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/registry/new-york/ui/select";
import { Separator } from "@/registry/new-york/ui/separator";

export interface ExportJob {
  id: string;
  format: "json" | "csv" | "pdf" | "zip";
  scope: string[];
  status: "pending" | "processing" | "completed" | "failed";
  progress?: number;
  createdAt: Date;
  completedAt?: Date;
  downloadUrl?: string;
  expiresAt?: Date;
  error?: string;
}

export interface SettingsExportDataProps {
  exportHistory?: ExportJob[];
  onExport?: (data: {
    format: "json" | "csv" | "pdf" | "zip";
    scope: string[];
    dateRange?: { start: Date; end: Date };
  }) => Promise<ExportJob>;
  onDownload?: (jobId: string) => Promise<void>;
  className?: string;
}

const exportFormats = [
  {
    value: "json",
    label: "JSON",
    description: "Machine-readable format, includes all data",
  },
  {
    value: "csv",
    label: "CSV",
    description: "Spreadsheet format, best for tabular data",
  },
  {
    value: "pdf",
    label: "PDF",
    description: "Human-readable document format",
  },
  {
    value: "zip",
    label: "ZIP",
    description: "Compressed archive with multiple formats",
  },
];

const dataCategories = [
  { id: "profile", label: "Profile Information" },
  { id: "activity", label: "Activity History" },
  { id: "messages", label: "Messages & Conversations" },
  { id: "files", label: "Uploaded Files" },
  { id: "settings", label: "Settings & Preferences" },
  { id: "billing", label: "Billing & Invoices" },
];

export default function SettingsExportData({
  exportHistory = [],
  onExport,
  onDownload,
  className,
}: SettingsExportDataProps) {
  const [isExporting, setIsExporting] = useState(false);
  const [selectedFormat, setSelectedFormat] = useState<
    "json" | "csv" | "pdf" | "zip"
  >("json");
  const [selectedCategories, setSelectedCategories] = useState<string[]>([]);
  const [dateRange, setDateRange] = useState({
    enabled: false,
    start: "",
    end: "",
  });

  const toggleCategory = (categoryId: string) => {
    setSelectedCategories((prev) =>
      prev.includes(categoryId)
        ? prev.filter((id) => id !== categoryId)
        : [...prev, categoryId]
    );
  };

  const handleExport = async () => {
    if (selectedCategories.length === 0) {
      return;
    }

    setIsExporting(true);
    try {
      const exportRange = dateRange.enabled
        ? {
            start: new Date(dateRange.start),
            end: new Date(dateRange.end),
          }
        : undefined;

      await onExport?.({
        format: selectedFormat,
        scope: selectedCategories,
        dateRange: exportRange,
      });

      setSelectedCategories([]);
      setDateRange({ enabled: false, start: "", end: "" });
    } finally {
      setIsExporting(false);
    }
  };

  const formatDate = (date: Date): string =>
    new Intl.DateTimeFormat("en-US", {
      month: "short",
      day: "numeric",
      year: "numeric",
      hour: "numeric",
      minute: "2-digit",
    }).format(date);

  const getStatusBadge = (status: ExportJob["status"]) => {
    switch (status) {
      case "completed":
        return (
          <Badge className="text-xs" variant="default">
            Completed
          </Badge>
        );
      case "processing":
        return (
          <Badge className="text-xs" variant="secondary">
            Processing
          </Badge>
        );
      case "pending":
        return (
          <Badge className="text-xs" variant="outline">
            Pending
          </Badge>
        );
      case "failed":
        return (
          <Badge className="text-xs" variant="destructive">
            Failed
          </Badge>
        );
    }
  };

  return (
    <Card className={cn("w-full shadow-xs", className)}>
      <CardHeader>
        <div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
          <div className="flex min-w-0 flex-1 flex-col gap-2">
            <CardTitle className="wrap-break-word">Export Data</CardTitle>
            <CardDescription className="wrap-break-word">
              Download a copy of your data in various formats
            </CardDescription>
          </div>
        </div>
      </CardHeader>
      <CardContent>
        <div className="flex flex-col gap-6">
          {/* Export Options */}
          <div className="flex flex-col gap-4">
            <Field>
              <FieldLabel htmlFor="export-format">Export Format</FieldLabel>
              <FieldContent>
                <Select
                  onValueChange={(value: "json" | "csv" | "pdf" | "zip") =>
                    setSelectedFormat(value)
                  }
                  value={selectedFormat}
                >
                  <SelectTrigger id="export-format">
                    <SelectValue>
                      {
                        exportFormats.find((f) => f.value === selectedFormat)
                          ?.label
                      }
                    </SelectValue>
                  </SelectTrigger>
                  <SelectContent>
                    {exportFormats.map((format) => (
                      <SelectItem key={format.value} value={format.value}>
                        <div className="flex flex-col">
                          <span>{format.label}</span>
                          <span className="text-muted-foreground text-xs">
                            {format.description}
                          </span>
                        </div>
                      </SelectItem>
                    ))}
                  </SelectContent>
                </Select>
              </FieldContent>
            </Field>

            <Field>
              <FieldLabel>Data Categories</FieldLabel>
              <FieldContent>
                <div className="flex flex-col gap-3">
                  {dataCategories.map((category) => (
                    <div
                      className="flex items-center gap-3 rounded-lg border p-3"
                      key={category.id}
                    >
                      <Checkbox
                        checked={selectedCategories.includes(category.id)}
                        id={`category-${category.id}`}
                        onCheckedChange={() => toggleCategory(category.id)}
                      />
                      <label
                        className="flex-1 cursor-pointer font-medium text-sm"
                        htmlFor={`category-${category.id}`}
                      >
                        {category.label}
                      </label>
                    </div>
                  ))}
                </div>
                <FieldDescription>
                  Select the data categories you want to export
                </FieldDescription>
              </FieldContent>
            </Field>

            <Field>
              <div className="flex items-center justify-between">
                <div className="flex flex-col gap-1">
                  <FieldLabel htmlFor="date-range">
                    Date Range (Optional)
                  </FieldLabel>
                  <FieldDescription>
                    Limit export to a specific date range
                  </FieldDescription>
                </div>
                <Checkbox
                  checked={dateRange.enabled}
                  id="date-range"
                  onCheckedChange={(checked) =>
                    setDateRange((prev) => ({
                      ...prev,
                      enabled: checked === true,
                    }))
                  }
                />
              </div>
            </Field>

            {dateRange.enabled && (
              <div className="flex flex-col gap-4 rounded-lg border bg-muted/30 p-4">
                <Field>
                  <FieldLabel htmlFor="date-start">Start Date</FieldLabel>
                  <FieldContent>
                    <InputGroup>
                      <InputGroupInput
                        id="date-start"
                        onChange={(e) =>
                          setDateRange((prev) => ({
                            ...prev,
                            start: e.target.value,
                          }))
                        }
                        type="date"
                        value={dateRange.start}
                      />
                    </InputGroup>
                  </FieldContent>
                </Field>

                <Field>
                  <FieldLabel htmlFor="date-end">End Date</FieldLabel>
                  <FieldContent>
                    <InputGroup>
                      <InputGroupInput
                        id="date-end"
                        onChange={(e) =>
                          setDateRange((prev) => ({
                            ...prev,
                            end: e.target.value,
                          }))
                        }
                        type="date"
                        value={dateRange.end}
                      />
                    </InputGroup>
                  </FieldContent>
                </Field>
              </div>
            )}

            <Button
              className="w-full sm:w-auto"
              disabled={isExporting || selectedCategories.length === 0}
              onClick={handleExport}
              type="button"
            >
              {isExporting ? (
                <>
                  <Loader2 className="size-4 animate-spin" />
                  Exporting…
                </>
              ) : (
                <>
                  <FileDown className="size-4" />
                  Start Export
                </>
              )}
            </Button>
          </div>

          <Separator />

          {/* Export History */}
          <div className="flex flex-col gap-4">
            <h3 className="font-semibold text-base">Export History</h3>
            {exportHistory.length === 0 ? (
              <p className="text-muted-foreground text-sm">
                No exports yet. Create your first export above.
              </p>
            ) : (
              <div className="flex flex-col gap-3">
                {exportHistory.map((job) => (
                  <div
                    className="flex flex-col gap-3 rounded-lg border p-4"
                    key={job.id}
                  >
                    <div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
                      <div className="flex min-w-0 flex-1 flex-col gap-2">
                        <div className="flex flex-wrap items-center gap-2">
                          <span className="font-medium text-sm">
                            {
                              exportFormats.find((f) => f.value === job.format)
                                ?.label
                            }
                          </span>
                          {getStatusBadge(job.status)}
                        </div>
                        <div className="flex flex-wrap gap-2">
                          {job.scope.map((category) => (
                            <Badge
                              className="text-xs"
                              key={category}
                              variant="outline"
                            >
                              {dataCategories.find((c) => c.id === category)
                                ?.label || category}
                            </Badge>
                          ))}
                        </div>
                        <div className="flex flex-wrap items-center gap-3 text-muted-foreground text-xs">
                          <span>Created: {formatDate(job.createdAt)}</span>
                          {job.completedAt && (
                            <span>
                              Completed: {formatDate(job.completedAt)}
                            </span>
                          )}
                          {job.expiresAt && (
                            <span>Expires: {formatDate(job.expiresAt)}</span>
                          )}
                        </div>
                        {job.status === "processing" &&
                          job.progress !== undefined && (
                            <div className="flex flex-col gap-2">
                              <Progress value={job.progress} />
                              <p className="text-muted-foreground text-xs">
                                {job.progress}% complete
                              </p>
                            </div>
                          )}
                        {job.error && (
                          <p className="text-destructive text-sm">
                            {job.error}
                          </p>
                        )}
                      </div>
                      {job.status === "completed" && job.downloadUrl && (
                        <Button
                          className="w-full sm:w-auto"
                          onClick={() => onDownload?.(job.id)}
                          type="button"
                          variant="outline"
                        >
                          <Download className="size-4" />
                          Download
                        </Button>
                      )}
                    </div>
                  </div>
                ))}
              </div>
            )}
          </div>
        </div>
      </CardContent>
    </Card>
  );
}

Installation

npx shadcn@latest add @hextaui/settings-export-data

Usage

import { SettingsExportData } from "@/components/ui/settings-export-data"
<SettingsExportData />