Settings Import Data
Settings Import Data is a React and Tailwind CSS component from HextaUI, licensed MIT. Copy it and paste it into your project.
HextaUI
MIT
other
What it is
Settings Import Data is a React and Tailwind CSS component from HextaUI, licensed MIT. Copy it and paste it into your project.
How to use it
Open it in the catalogue, press copy, and paste it into your project. The prompt you copy carries the code and the rules that tell your agent to reproduce it without changing anything.
Five free copies a month. No card.
No packages beyond the project baseline
Licence
This component comes from HextaUI and is redistributed under the MIT licence, which permits commercial use. The copyright notice travels with the code.
Settings Import Data
"use client";
import { AlertCircle, Check, FileUp, Loader2, Upload, X } from "lucide-react";
import { useCallback, useRef, 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 { Field, FieldContent, FieldLabel } from "@/registry/new-york/ui/field";
import { Progress } from "@/registry/new-york/ui/progress";
import { RadioGroup, RadioGroupItem } from "@/registry/new-york/ui/radio-group";
import { Separator } from "@/registry/new-york/ui/separator";
export interface ImportPreview {
totalRecords: number;
categories: Record<string, number>;
conflicts: number;
fields: string[];
}
export interface ImportJob {
id: string;
filename: string;
format: "json" | "csv";
status: "preview" | "dry-run" | "importing" | "completed" | "failed";
progress?: number;
preview?: ImportPreview;
conflictResolution?: "skip" | "overwrite" | "merge";
createdAt: Date;
completedAt?: Date;
error?: string;
recordsImported?: number;
recordsSkipped?: number;
recordsFailed?: number;
}
export interface SettingsImportDataProps {
importHistory?: ImportJob[];
onUpload?: (file: File) => Promise<ImportPreview>;
onImport?: (data: {
file: File;
conflictResolution: "skip" | "overwrite" | "merge";
dryRun?: boolean;
}) => Promise<ImportJob>;
className?: string;
}
const conflictResolutionOptions = [
{
value: "skip",
label: "Skip",
description: "Skip conflicting records, keep existing data",
},
{
value: "overwrite",
label: "Overwrite",
description: "Replace existing data with imported data",
},
{
value: "merge",
label: "Merge",
description: "Combine existing and imported data",
},
];
function formatFileSize(bytes: number): string {
if (bytes === 0) return "0 Bytes";
const k = 1024;
const sizes = ["Bytes", "KB", "MB", "GB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return `${Math.round((bytes / k ** i) * 100) / 100} ${sizes[i]}`;
}
export default function SettingsImportData({
importHistory = [],
onUpload,
onImport,
className,
}: SettingsImportDataProps) {
const [isDragging, setIsDragging] = useState(false);
const [isUploading, setIsUploading] = useState(false);
const [isImporting, setIsImporting] = useState(false);
const [uploadedFile, setUploadedFile] = useState<File | null>(null);
const [preview, setPreview] = useState<ImportPreview | null>(null);
const [conflictResolution, setConflictResolution] = useState<
"skip" | "overwrite" | "merge"
>("skip");
const [errors, setErrors] = useState<Record<string, string>>({});
const fileInputRef = useRef<HTMLInputElement>(null);
const handleFileSelect = useCallback(
async (file: File) => {
setErrors({});
if (!(file.name.endsWith(".json") || file.name.endsWith(".csv"))) {
setErrors({
file: "Please upload a JSON or CSV file",
});
return;
}
if (file.size > 50 * 1024 * 1024) {
setErrors({
file: "File size must be less than 50MB",
});
return;
}
setUploadedFile(file);
setIsUploading(true);
try {
const previewData = await onUpload?.(file);
if (previewData) {
setPreview(previewData);
}
} catch (error) {
setErrors({
file:
error instanceof Error ? error.message : "Failed to process file",
});
setUploadedFile(null);
} finally {
setIsUploading(false);
}
},
[onUpload]
);
const handleDragOver = useCallback((e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
setIsDragging(true);
}, []);
const handleDragLeave = useCallback((e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
setIsDragging(false);
}, []);
const handleDrop = useCallback(
(e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
setIsDragging(false);
const file = e.dataTransfer.files[0];
if (file) {
handleFileSelect(file);
}
},
[handleFileSelect]
);
const handleFileInputChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
handleFileSelect(file);
}
},
[handleFileSelect]
);
const handleImport = async (dryRun = false) => {
if (!uploadedFile) return;
setIsImporting(true);
try {
await onImport?.({
file: uploadedFile,
conflictResolution,
dryRun,
});
if (!dryRun) {
setUploadedFile(null);
setPreview(null);
setConflictResolution("skip");
}
} catch (error) {
setErrors({
import:
error instanceof Error ? error.message : "Failed to import data",
});
} finally {
setIsImporting(false);
}
};
const handleRemove = () => {
setUploadedFile(null);
setPreview(null);
setErrors({});
if (fileInputRef.current) {
fileInputRef.current.value = "";
}
};
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: ImportJob["status"]) => {
switch (status) {
case "completed":
return (
<Badge className="text-xs" variant="default">
Completed
</Badge>
);
case "importing":
return (
<Badge className="text-xs" variant="secondary">
Importing
</Badge>
);
case "dry-run":
return (
<Badge className="text-xs" variant="outline">
Dry Run
</Badge>
);
case "failed":
return (
<Badge className="text-xs" variant="destructive">
Failed
</Badge>
);
default:
return null;
}
};
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">Import Data</CardTitle>
<CardDescription className="wrap-break-word">
Import your data from a JSON or CSV file
</CardDescription>
</div>
</div>
</CardHeader>
<CardContent>
<div className="flex flex-col gap-6">
{/* File Upload */}
<div className="flex flex-col gap-4">
{uploadedFile ? (
<div className="flex flex-col gap-4 rounded-lg border p-4">
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
<div className="flex min-w-0 flex-1 items-center gap-3">
<div className="flex size-10 shrink-0 items-center justify-center rounded-lg bg-primary/10">
<FileUp className="size-5 text-primary" />
</div>
<div className="flex min-w-0 flex-1 flex-col gap-2">
<p className="wrap-break-word font-medium text-sm">
{uploadedFile.name}
</p>
<p className="text-muted-foreground text-xs">
{formatFileSize(uploadedFile.size)}
</p>
</div>
</div>
<Button
className="self-start sm:self-auto"
onClick={handleRemove}
size="icon-sm"
type="button"
variant="ghost"
>
<X className="size-4" />
</Button>
</div>
{preview && (
<>
<Separator />
<div className="flex flex-col gap-3">
<h4 className="font-medium text-sm">Preview</h4>
<div className="flex flex-col gap-2 rounded-lg bg-muted/30 p-3">
<div className="flex items-center justify-between">
<span className="text-muted-foreground text-sm">
Total Records:
</span>
<span className="font-medium text-sm">
{preview.totalRecords}
</span>
</div>
{preview.conflicts > 0 && (
<div className="flex items-center justify-between">
<span className="text-muted-foreground text-sm">
Conflicts:
</span>
<Badge className="text-xs" variant="destructive">
{preview.conflicts}
</Badge>
</div>
)}
{Object.keys(preview.categories).length > 0 && (
<div className="flex flex-col gap-1">
<span className="text-muted-foreground text-sm">
Categories:
</span>
<div className="flex flex-wrap gap-1">
{Object.entries(preview.categories).map(
([category, count]) => (
<Badge
className="text-xs"
key={category}
variant="outline"
>
{category}: {count}
</Badge>
)
)}
</div>
</div>
)}
</div>
</div>
</>
)}
</div>
) : (
<div
className={cn(
"flex cursor-pointer flex-col items-center justify-center gap-4 rounded-lg border-2 border-dashed p-8 transition-colors",
isDragging
? "border-primary bg-primary/5"
: "border-muted bg-muted/30 hover:border-primary/50"
)}
onClick={() => fileInputRef.current?.click()}
onDragLeave={handleDragLeave}
onDragOver={handleDragOver}
onDrop={handleDrop}
>
{isUploading ? (
<Loader2 className="size-8 animate-spin text-primary" />
) : (
<Upload className="size-8 text-muted-foreground" />
)}
<div className="flex flex-col gap-2 text-center">
<p className="font-medium text-sm">
{isUploading
? "Processing file…"
: "Drag and drop a file here, or click to browse"}
</p>
<p className="text-muted-foreground text-xs">
Supported formats: JSON, CSV • Max size: 50MB
</p>
</div>
<input
accept=".json,.csv"
className="hidden"
onChange={handleFileInputChange}
ref={fileInputRef}
type="file"
/>
</div>
)}
{errors.file && (
<div className="flex items-center gap-2 rounded-lg border border-destructive/50 bg-destructive/10 p-3">
<AlertCircle className="size-4 text-destructive" />
<p className="text-destructive text-sm">{errors.file}</p>
</div>
)}
</div>
{/* Import Options */}
{uploadedFile && preview && (
<>
<Separator />
<div className="flex flex-col gap-4">
<h3 className="font-semibold text-base">Import Options</h3>
<Field>
<FieldLabel>Conflict Resolution</FieldLabel>
<FieldContent>
<RadioGroup
onValueChange={(value: "skip" | "overwrite" | "merge") =>
setConflictResolution(value)
}
value={conflictResolution}
>
{conflictResolutionOptions.map((option) => (
<div
className="flex items-start gap-3 rounded-lg border p-3"
key={option.value}
>
<RadioGroupItem
id={`resolution-${option.value}`}
value={option.value}
/>
<div className="flex flex-1 flex-col gap-1">
<label
className="cursor-pointer font-medium text-sm"
htmlFor={`resolution-${option.value}`}
>
{option.label}
</label>
<p className="text-muted-foreground text-xs">
{option.description}
</p>
</div>
</div>
))}
</RadioGroup>
</FieldContent>
</Field>
<div className="flex flex-col gap-2 sm:flex-row">
<Button
className="w-full sm:w-auto"
disabled={isImporting}
onClick={() => handleImport(true)}
type="button"
variant="outline"
>
<Check className="size-4" />
Dry Run
</Button>
<Button
className="w-full sm:w-auto"
disabled={isImporting}
onClick={() => handleImport(false)}
type="button"
>
{isImporting ? (
<>
<Loader2 className="size-4 animate-spin" />
Importing…
</>
) : (
<>
<FileUp className="size-4" />
Import Data
</>
)}
</Button>
</div>
{errors.import && (
<div className="flex items-center gap-2 rounded-lg border border-destructive/50 bg-destructive/10 p-3">
<AlertCircle className="size-4 text-destructive" />
<p className="text-destructive text-sm">{errors.import}</p>
</div>
)}
</div>
</>
)}
<Separator />
{/* Import History */}
<div className="flex flex-col gap-4">
<h3 className="font-semibold text-base">Import History</h3>
{importHistory.length === 0 ? (
<p className="text-muted-foreground text-sm">
No imports yet. Upload a file above to get started.
</p>
) : (
<div className="flex flex-col gap-3">
{importHistory.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">
{job.filename}
</span>
{getStatusBadge(job.status)}
<Badge
className="text-xs uppercase"
variant="outline"
>
{job.format}
</Badge>
</div>
{job.status === "importing" &&
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.status === "completed" && (
<div className="flex flex-wrap items-center gap-3 text-muted-foreground text-xs">
{job.recordsImported !== undefined && (
<span className="text-green-600">
{job.recordsImported} imported
</span>
)}
{job.recordsSkipped !== undefined &&
job.recordsSkipped > 0 && (
<span>{job.recordsSkipped} skipped</span>
)}
{job.recordsFailed !== undefined &&
job.recordsFailed > 0 && (
<span className="text-destructive">
{job.recordsFailed} failed
</span>
)}
</div>
)}
<div className="flex flex-wrap items-center gap-3 text-muted-foreground text-xs">
<span>Started: {formatDate(job.createdAt)}</span>
{job.completedAt && (
<span>
Completed: {formatDate(job.completedAt)}
</span>
)}
</div>
{job.error && (
<p className="text-destructive text-sm">
{job.error}
</p>
)}
</div>
</div>
</div>
))}
</div>
)}
</div>
</div>
</CardContent>
</Card>
);
}