Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 65 additions & 4 deletions frontend/src/components/CSVUploader.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,17 @@
import React, { useState, useRef, useCallback, useId } from 'react';
import { Upload, AlertCircle, CheckCircle, XCircle, FileSpreadsheet, Loader2 } from 'lucide-react';
import {
Upload,
AlertCircle,
CheckCircle,
XCircle,
FileSpreadsheet,
Loader2,
Download,
} from 'lucide-react';
import { useNotification } from '../hooks/useNotification';

const MAX_FILE_SIZE = 10 * 1024 * 1024;

export interface CSVRow {
rowNumber: number;
data: Record<string, string>;
Expand Down Expand Up @@ -123,6 +133,13 @@ export const CSVUploader: React.FC<CSVUploaderProps> = ({
return;
}

if (file.size > MAX_FILE_SIZE) {
const maxSizeMB = (MAX_FILE_SIZE / (1024 * 1024)).toFixed(1);
setParseError(`File size exceeds ${maxSizeMB}MB limit. Please choose a smaller file.`);
notifyError('File too large', `Maximum file size is ${maxSizeMB}MB.`);
return;
}

setFileName(file.name);
setIsLoading(true);
const reader = new FileReader();
Expand Down Expand Up @@ -214,6 +231,35 @@ export const CSVUploader: React.FC<CSVUploaderProps> = ({
}
};

const generateErrorReport = useCallback(() => {
const errorRows = parsedData.filter((r) => !r.isValid);
if (errorRows.length === 0) return;

const columns = ['Row Number', ...Object.keys(parsedData[0]?.data || {}), 'Errors'];
const csvContent = [
columns.map((col) => `"${col}"`).join(','),
...errorRows.map((row) => {
const values = [
row.rowNumber.toString(),
...Object.values(row.data).map((val) => `"${val}"`),
`"${row.errors.join('; ')}"`,
];
return values.join(',');
}),
].join('\n');

const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
const url = URL.createObjectURL(blob);
const timestamp = new Date().toISOString().split('T')[0];
link.setAttribute('href', url);
link.setAttribute('download', `error-report-${timestamp}.csv`);
link.style.visibility = 'hidden';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}, [parsedData]);

const validRowsCount = parsedData.filter((r) => r.isValid).length;
const invalidRowsCount = parsedData.filter((r) => !r.isValid).length;
const hasData = parsedData.length > 0;
Expand Down Expand Up @@ -309,9 +355,24 @@ export const CSVUploader: React.FC<CSVUploaderProps> = ({
aria-live="polite"
aria-label={`File summary: ${fileName}`}
>
<div className="flex items-center gap-2">
<FileSpreadsheet className="w-4 h-4 text-[var(--accent)] shrink-0" aria-hidden="true" />
<p className="text-sm font-semibold text-[var(--text)] truncate">{fileName}</p>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<FileSpreadsheet
className="w-4 h-4 text-[var(--accent)] shrink-0"
aria-hidden="true"
/>
<p className="text-sm font-semibold text-[var(--text)] truncate">{fileName}</p>
</div>
{invalidRowsCount > 0 && (
<button
onClick={generateErrorReport}
className="ml-2 inline-flex items-center gap-1.5 rounded-lg bg-[var(--accent)] text-[var(--bg)] px-3 py-1.5 text-xs font-medium hover:bg-[var(--accent)]/90 transition-colors focus:outline-none focus:ring-2 focus:ring-[var(--accent)]/50"
title="Download error report as CSV"
>
<Download className="w-3.5 h-3.5" aria-hidden="true" />
Export errors
</button>
)}
</div>
<div className="mt-3 flex flex-wrap gap-3 text-sm">
<span className="inline-flex items-center gap-1.5 rounded-full bg-[rgba(63,185,80,0.1)] px-3 py-1 text-xs font-medium text-[var(--success)]">
Expand Down
Loading