diff --git a/apps/web/components/CSVUpload.tsx b/apps/web/components/CSVUpload.tsx new file mode 100644 index 0000000..7fda7c6 --- /dev/null +++ b/apps/web/components/CSVUpload.tsx @@ -0,0 +1,96 @@ +import { + Button, + FileButton, + Group, + SimpleGrid, + Space, + Stack, + Text, +} from '@mantine/core'; +import { useState } from 'react'; +import { z } from 'zod'; +import { parseCSV } from 'zod-csv'; + +// Schema to validate each row +const rowSchema = z.record(z.string()); // Flexible schema for dynamic column detection +const phoneNumberSchema = z + .string() + .regex(/^\+?[1-9]\d{1,14}$/, 'Invalid phone number'); + +export default function CSVUpload({ + handleUpload, + handleDelete, +}: { + handleUpload: (data: Array<{ phone_number: string }>) => Promise; + handleDelete: () => void; +}) { + const [file, setFile] = useState(null); + const [error, setError] = useState(null); + + const handleFileChange = async (f: File | null) => { + if (!f) return; // Ensure `f` is a `File` + setFile(f); + + try { + const parsedCSV = await parseCSV(f, rowSchema); // Directly parse the File object + if (!parsedCSV.success) { + throw new Error('Invalid CSV format'); + } + + // Use validRows to process data + const rows = parsedCSV.validRows; + const columns = parsedCSV.header; + const phoneColumn = columns.find((col) => + rows.every((row) => phoneNumberSchema.safeParse(row[col]).success) + ); + + if (!phoneColumn) { + throw new Error('No valid phone number column found'); + } + + // Extract and format data for upload + const phoneData = rows.map((row) => ({ + phone_number: row[phoneColumn], + })); + + await handleUpload(phoneData); + setError(null); + } catch (err) { + setError((err as Error).message); + } + }; + + return ( + + + + handleFileChange(f)} accept='.csv'> + {(props) => ( + + )} + + + {file && Selected File: {file.name}} + {error && ( + + {error} + + )} + + + + + ); +} diff --git a/package-lock.json b/package-lock.json index 0313067..ef76525 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,9 @@ "apps/*", "packages/*" ], + "dependencies": { + "zod-csv": "^0.0.8" + }, "devDependencies": { "@messaging/eslint-config": "*", "@messaging/prettier-config": "*", @@ -2826,6 +2829,14 @@ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" }, + "node_modules/csv-string": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/csv-string/-/csv-string-4.1.1.tgz", + "integrity": "sha512-KGvaJEZEdh2O/EVvczwbPLqJZtSQaWQ4cEJbiOJEG4ALq+dBBqNmBkRXTF4NV79V25+XYtiqbco1IWrmHLm5FQ==", + "engines": { + "node": ">=12.0" + } + }, "node_modules/damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", @@ -8916,6 +8927,17 @@ "url": "https://github.com/sponsors/colinhacks" } }, + "node_modules/zod-csv": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/zod-csv/-/zod-csv-0.0.8.tgz", + "integrity": "sha512-ObIUbDTAMkVJ5bhHqIg93X9HpOyV7V/qEgtZSUCITF1IlG3oXoX3EvEqXicJjZ1tu0q66BHcaCG0BFXF54wKBA==", + "dependencies": { + "csv-string": "^4.1.1" + }, + "peerDependencies": { + "zod": "^3.11.x" + } + }, "node_modules/zod-form-data": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/zod-form-data/-/zod-form-data-2.0.2.tgz", diff --git a/package.json b/package.json index 3da3c9e..06474e8 100644 --- a/package.json +++ b/package.json @@ -26,13 +26,13 @@ }, "packageManager": "npm@10.2.4", "devDependencies": { + "@messaging/eslint-config": "*", + "@messaging/prettier-config": "*", "husky": "^8.0.0", "lint-staged": "^15.2.5", - "typescript": "^5.0.4", - "vercel": "^33.5.1", "turbo": "^2.0.3", - "@messaging/prettier-config": "*", - "@messaging/eslint-config": "*" + "typescript": "^5.0.4", + "vercel": "^33.5.1" }, "prettier": "@messaging/prettier-config", "eslintConfig": { @@ -40,5 +40,8 @@ "env": { "browser": false } + }, + "dependencies": { + "zod-csv": "^0.0.8" } -} \ No newline at end of file +}