jtcsv 2.1.3 → 2.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +60 -341
- package/bin/jtcsv.js +2462 -1372
- package/csv-to-json.js +35 -26
- package/dist/jtcsv.cjs.js +807 -133
- package/dist/jtcsv.cjs.js.map +1 -1
- package/dist/jtcsv.esm.js +800 -134
- package/dist/jtcsv.esm.js.map +1 -1
- package/dist/jtcsv.umd.js +807 -133
- package/dist/jtcsv.umd.js.map +1 -1
- package/errors.js +20 -0
- package/examples/browser-vanilla.html +37 -0
- package/examples/cli-batch-processing.js +38 -0
- package/examples/error-handling.js +324 -0
- package/examples/ndjson-processing.js +434 -0
- package/examples/react-integration.jsx +637 -0
- package/examples/schema-validation.js +640 -0
- package/examples/simple-usage.js +10 -7
- package/examples/typescript-example.ts +486 -0
- package/examples/web-workers-advanced.js +28 -0
- package/index.d.ts +2 -0
- package/json-save.js +2 -1
- package/json-to-csv.js +171 -131
- package/package.json +20 -4
- package/plugins/README.md +41 -467
- package/plugins/express-middleware/README.md +32 -274
- package/plugins/hono/README.md +16 -13
- package/plugins/nestjs/README.md +13 -11
- package/plugins/nextjs-api/README.md +28 -423
- package/plugins/nextjs-api/index.js +1 -2
- package/plugins/nextjs-api/route.js +1 -2
- package/plugins/nuxt/README.md +6 -7
- package/plugins/remix/README.md +9 -9
- package/plugins/sveltekit/README.md +8 -8
- package/plugins/trpc/README.md +8 -5
- package/src/browser/browser-functions.js +33 -3
- package/src/browser/csv-to-json-browser.js +269 -11
- package/src/browser/errors-browser.js +19 -1
- package/src/browser/index.js +39 -5
- package/src/browser/streams.js +393 -0
- package/src/browser/workers/csv-parser.worker.js +20 -2
- package/src/browser/workers/worker-pool.js +507 -447
- package/src/core/plugin-system.js +4 -0
- package/src/engines/fast-path-engine.js +31 -23
- package/src/errors.js +26 -0
- package/src/formats/ndjson-parser.js +54 -5
- package/src/formats/tsv-parser.js +4 -1
- package/src/utils/schema-validator.js +594 -0
- package/src/utils/transform-loader.js +205 -0
- package/src/web-server/index.js +683 -0
- package/stream-csv-to-json.js +16 -87
- package/stream-json-to-csv.js +18 -86
|
@@ -0,0 +1,637 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React Integration Example for jtcsv
|
|
3
|
+
*
|
|
4
|
+
* This file demonstrates how to use jtcsv in React applications
|
|
5
|
+
* for CSV import/export functionality.
|
|
6
|
+
*
|
|
7
|
+
* To use: npm install jtcsv react react-dom
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import React, { useState, useCallback, useMemo } from 'react';
|
|
11
|
+
import { csvToJson, jsonToCsv, ValidationError, ParsingError } from 'jtcsv';
|
|
12
|
+
|
|
13
|
+
// =============================================================================
|
|
14
|
+
// Example 1: CSV Import Component
|
|
15
|
+
// =============================================================================
|
|
16
|
+
|
|
17
|
+
export function CsvImporter({ onImport, columns, parseOptions = {} }) {
|
|
18
|
+
const [error, setError] = useState(null);
|
|
19
|
+
const [preview, setPreview] = useState(null);
|
|
20
|
+
const [isProcessing, setIsProcessing] = useState(false);
|
|
21
|
+
|
|
22
|
+
const handleFileSelect = useCallback(async (event) => {
|
|
23
|
+
const file = event.target.files?.[0];
|
|
24
|
+
if (!file) return;
|
|
25
|
+
|
|
26
|
+
setError(null);
|
|
27
|
+
setIsProcessing(true);
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
const text = await file.text();
|
|
31
|
+
const data = csvToJson(text, {
|
|
32
|
+
hasHeaders: true,
|
|
33
|
+
trim: true,
|
|
34
|
+
parseNumbers: true,
|
|
35
|
+
parseBooleans: true,
|
|
36
|
+
...parseOptions
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// Show preview of first 5 rows
|
|
40
|
+
setPreview({
|
|
41
|
+
total: data.length,
|
|
42
|
+
sample: data.slice(0, 5),
|
|
43
|
+
columns: data.length > 0 ? Object.keys(data[0]) : []
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// Call onImport with full data
|
|
47
|
+
if (onImport) {
|
|
48
|
+
onImport(data);
|
|
49
|
+
}
|
|
50
|
+
} catch (err) {
|
|
51
|
+
if (err instanceof ParsingError) {
|
|
52
|
+
setError(`Parsing error at line ${err.lineNumber}: ${err.message}`);
|
|
53
|
+
} else if (err instanceof ValidationError) {
|
|
54
|
+
setError(`Validation error: ${err.message}`);
|
|
55
|
+
} else {
|
|
56
|
+
setError(`Error: ${err.message}`);
|
|
57
|
+
}
|
|
58
|
+
setPreview(null);
|
|
59
|
+
} finally {
|
|
60
|
+
setIsProcessing(false);
|
|
61
|
+
}
|
|
62
|
+
}, [onImport, parseOptions]);
|
|
63
|
+
|
|
64
|
+
const clearPreview = useCallback(() => {
|
|
65
|
+
setPreview(null);
|
|
66
|
+
setError(null);
|
|
67
|
+
}, []);
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<div className="csv-importer">
|
|
71
|
+
<div className="upload-area">
|
|
72
|
+
<input
|
|
73
|
+
type="file"
|
|
74
|
+
accept=".csv,.tsv,.txt"
|
|
75
|
+
onChange={handleFileSelect}
|
|
76
|
+
disabled={isProcessing}
|
|
77
|
+
/>
|
|
78
|
+
{isProcessing && <span>Processing...</span>}
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
{error && (
|
|
82
|
+
<div className="error-message" style={{ color: 'red' }}>
|
|
83
|
+
{error}
|
|
84
|
+
</div>
|
|
85
|
+
)}
|
|
86
|
+
|
|
87
|
+
{preview && (
|
|
88
|
+
<div className="preview">
|
|
89
|
+
<h4>Preview ({preview.total} rows total)</h4>
|
|
90
|
+
<p>Columns: {preview.columns.join(', ')}</p>
|
|
91
|
+
<table>
|
|
92
|
+
<thead>
|
|
93
|
+
<tr>
|
|
94
|
+
{preview.columns.map(col => (
|
|
95
|
+
<th key={col}>{col}</th>
|
|
96
|
+
))}
|
|
97
|
+
</tr>
|
|
98
|
+
</thead>
|
|
99
|
+
<tbody>
|
|
100
|
+
{preview.sample.map((row, i) => (
|
|
101
|
+
<tr key={i}>
|
|
102
|
+
{preview.columns.map(col => (
|
|
103
|
+
<td key={col}>{String(row[col])}</td>
|
|
104
|
+
))}
|
|
105
|
+
</tr>
|
|
106
|
+
))}
|
|
107
|
+
</tbody>
|
|
108
|
+
</table>
|
|
109
|
+
<button onClick={clearPreview}>Clear</button>
|
|
110
|
+
</div>
|
|
111
|
+
)}
|
|
112
|
+
</div>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// =============================================================================
|
|
117
|
+
// Example 2: CSV Export Component
|
|
118
|
+
// =============================================================================
|
|
119
|
+
|
|
120
|
+
export function CsvExporter({ data, filename = 'export.csv', options = {} }) {
|
|
121
|
+
const [isExporting, setIsExporting] = useState(false);
|
|
122
|
+
|
|
123
|
+
const handleExport = useCallback(() => {
|
|
124
|
+
setIsExporting(true);
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
const csv = jsonToCsv(data, {
|
|
128
|
+
delimiter: ',',
|
|
129
|
+
includeHeaders: true,
|
|
130
|
+
preventCsvInjection: true,
|
|
131
|
+
...options
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// Create blob and download
|
|
135
|
+
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
|
|
136
|
+
const url = URL.createObjectURL(blob);
|
|
137
|
+
const link = document.createElement('a');
|
|
138
|
+
link.href = url;
|
|
139
|
+
link.download = filename;
|
|
140
|
+
document.body.appendChild(link);
|
|
141
|
+
link.click();
|
|
142
|
+
document.body.removeChild(link);
|
|
143
|
+
URL.revokeObjectURL(url);
|
|
144
|
+
} catch (err) {
|
|
145
|
+
console.error('Export failed:', err);
|
|
146
|
+
alert(`Export failed: ${err.message}`);
|
|
147
|
+
} finally {
|
|
148
|
+
setIsExporting(false);
|
|
149
|
+
}
|
|
150
|
+
}, [data, filename, options]);
|
|
151
|
+
|
|
152
|
+
return (
|
|
153
|
+
<button
|
|
154
|
+
onClick={handleExport}
|
|
155
|
+
disabled={isExporting || !data || data.length === 0}
|
|
156
|
+
>
|
|
157
|
+
{isExporting ? 'Exporting...' : `Export CSV (${data?.length || 0} rows)`}
|
|
158
|
+
</button>
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// =============================================================================
|
|
163
|
+
// Example 3: Custom Hook for CSV Operations
|
|
164
|
+
// =============================================================================
|
|
165
|
+
|
|
166
|
+
export function useCsv(initialData = []) {
|
|
167
|
+
const [data, setData] = useState(initialData);
|
|
168
|
+
const [error, setError] = useState(null);
|
|
169
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
170
|
+
|
|
171
|
+
const importCsv = useCallback(async (csvString, options = {}) => {
|
|
172
|
+
setIsLoading(true);
|
|
173
|
+
setError(null);
|
|
174
|
+
|
|
175
|
+
try {
|
|
176
|
+
const parsed = csvToJson(csvString, {
|
|
177
|
+
hasHeaders: true,
|
|
178
|
+
trim: true,
|
|
179
|
+
...options
|
|
180
|
+
});
|
|
181
|
+
setData(parsed);
|
|
182
|
+
return parsed;
|
|
183
|
+
} catch (err) {
|
|
184
|
+
setError(err);
|
|
185
|
+
throw err;
|
|
186
|
+
} finally {
|
|
187
|
+
setIsLoading(false);
|
|
188
|
+
}
|
|
189
|
+
}, []);
|
|
190
|
+
|
|
191
|
+
const exportCsv = useCallback((options = {}) => {
|
|
192
|
+
try {
|
|
193
|
+
return jsonToCsv(data, {
|
|
194
|
+
delimiter: ',',
|
|
195
|
+
includeHeaders: true,
|
|
196
|
+
...options
|
|
197
|
+
});
|
|
198
|
+
} catch (err) {
|
|
199
|
+
setError(err);
|
|
200
|
+
throw err;
|
|
201
|
+
}
|
|
202
|
+
}, [data]);
|
|
203
|
+
|
|
204
|
+
const importFromFile = useCallback(async (file, options = {}) => {
|
|
205
|
+
const text = await file.text();
|
|
206
|
+
return importCsv(text, options);
|
|
207
|
+
}, [importCsv]);
|
|
208
|
+
|
|
209
|
+
const downloadCsv = useCallback((filename = 'data.csv', options = {}) => {
|
|
210
|
+
const csv = exportCsv(options);
|
|
211
|
+
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
|
|
212
|
+
const url = URL.createObjectURL(blob);
|
|
213
|
+
const link = document.createElement('a');
|
|
214
|
+
link.href = url;
|
|
215
|
+
link.download = filename;
|
|
216
|
+
link.click();
|
|
217
|
+
URL.revokeObjectURL(url);
|
|
218
|
+
}, [exportCsv]);
|
|
219
|
+
|
|
220
|
+
const clearData = useCallback(() => {
|
|
221
|
+
setData([]);
|
|
222
|
+
setError(null);
|
|
223
|
+
}, []);
|
|
224
|
+
|
|
225
|
+
return {
|
|
226
|
+
data,
|
|
227
|
+
setData,
|
|
228
|
+
error,
|
|
229
|
+
isLoading,
|
|
230
|
+
importCsv,
|
|
231
|
+
exportCsv,
|
|
232
|
+
importFromFile,
|
|
233
|
+
downloadCsv,
|
|
234
|
+
clearData
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// =============================================================================
|
|
239
|
+
// Example 4: Data Table with CSV Support
|
|
240
|
+
// =============================================================================
|
|
241
|
+
|
|
242
|
+
export function DataTableWithCsv({
|
|
243
|
+
initialData = [],
|
|
244
|
+
columns,
|
|
245
|
+
editable = false,
|
|
246
|
+
onDataChange
|
|
247
|
+
}) {
|
|
248
|
+
const {
|
|
249
|
+
data,
|
|
250
|
+
setData,
|
|
251
|
+
importFromFile,
|
|
252
|
+
downloadCsv,
|
|
253
|
+
isLoading,
|
|
254
|
+
error
|
|
255
|
+
} = useCsv(initialData);
|
|
256
|
+
|
|
257
|
+
const handleFileChange = useCallback(async (event) => {
|
|
258
|
+
const file = event.target.files?.[0];
|
|
259
|
+
if (file) {
|
|
260
|
+
try {
|
|
261
|
+
const imported = await importFromFile(file, {
|
|
262
|
+
parseNumbers: true,
|
|
263
|
+
parseBooleans: true
|
|
264
|
+
});
|
|
265
|
+
if (onDataChange) onDataChange(imported);
|
|
266
|
+
} catch (err) {
|
|
267
|
+
// Error is handled by useCsv hook
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}, [importFromFile, onDataChange]);
|
|
271
|
+
|
|
272
|
+
const handleCellEdit = useCallback((rowIndex, column, value) => {
|
|
273
|
+
const newData = [...data];
|
|
274
|
+
newData[rowIndex] = { ...newData[rowIndex], [column]: value };
|
|
275
|
+
setData(newData);
|
|
276
|
+
if (onDataChange) onDataChange(newData);
|
|
277
|
+
}, [data, setData, onDataChange]);
|
|
278
|
+
|
|
279
|
+
const handleAddRow = useCallback(() => {
|
|
280
|
+
const emptyRow = columns.reduce((acc, col) => ({ ...acc, [col]: '' }), {});
|
|
281
|
+
const newData = [...data, emptyRow];
|
|
282
|
+
setData(newData);
|
|
283
|
+
if (onDataChange) onDataChange(newData);
|
|
284
|
+
}, [data, columns, setData, onDataChange]);
|
|
285
|
+
|
|
286
|
+
const handleDeleteRow = useCallback((rowIndex) => {
|
|
287
|
+
const newData = data.filter((_, i) => i !== rowIndex);
|
|
288
|
+
setData(newData);
|
|
289
|
+
if (onDataChange) onDataChange(newData);
|
|
290
|
+
}, [data, setData, onDataChange]);
|
|
291
|
+
|
|
292
|
+
const displayColumns = columns || (data.length > 0 ? Object.keys(data[0]) : []);
|
|
293
|
+
|
|
294
|
+
return (
|
|
295
|
+
<div className="data-table">
|
|
296
|
+
<div className="toolbar">
|
|
297
|
+
<input
|
|
298
|
+
type="file"
|
|
299
|
+
accept=".csv"
|
|
300
|
+
onChange={handleFileChange}
|
|
301
|
+
disabled={isLoading}
|
|
302
|
+
/>
|
|
303
|
+
<button onClick={() => downloadCsv('export.csv')}>
|
|
304
|
+
Export CSV
|
|
305
|
+
</button>
|
|
306
|
+
{editable && (
|
|
307
|
+
<button onClick={handleAddRow}>Add Row</button>
|
|
308
|
+
)}
|
|
309
|
+
</div>
|
|
310
|
+
|
|
311
|
+
{error && (
|
|
312
|
+
<div className="error" style={{ color: 'red' }}>
|
|
313
|
+
{error.message}
|
|
314
|
+
</div>
|
|
315
|
+
)}
|
|
316
|
+
|
|
317
|
+
{isLoading && <div>Loading...</div>}
|
|
318
|
+
|
|
319
|
+
<table>
|
|
320
|
+
<thead>
|
|
321
|
+
<tr>
|
|
322
|
+
{displayColumns.map(col => (
|
|
323
|
+
<th key={col}>{col}</th>
|
|
324
|
+
))}
|
|
325
|
+
{editable && <th>Actions</th>}
|
|
326
|
+
</tr>
|
|
327
|
+
</thead>
|
|
328
|
+
<tbody>
|
|
329
|
+
{data.map((row, rowIndex) => (
|
|
330
|
+
<tr key={rowIndex}>
|
|
331
|
+
{displayColumns.map(col => (
|
|
332
|
+
<td key={col}>
|
|
333
|
+
{editable ? (
|
|
334
|
+
<input
|
|
335
|
+
value={row[col] ?? ''}
|
|
336
|
+
onChange={(e) => handleCellEdit(rowIndex, col, e.target.value)}
|
|
337
|
+
/>
|
|
338
|
+
) : (
|
|
339
|
+
String(row[col] ?? '')
|
|
340
|
+
)}
|
|
341
|
+
</td>
|
|
342
|
+
))}
|
|
343
|
+
{editable && (
|
|
344
|
+
<td>
|
|
345
|
+
<button onClick={() => handleDeleteRow(rowIndex)}>Delete</button>
|
|
346
|
+
</td>
|
|
347
|
+
)}
|
|
348
|
+
</tr>
|
|
349
|
+
))}
|
|
350
|
+
</tbody>
|
|
351
|
+
</table>
|
|
352
|
+
</div>
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// =============================================================================
|
|
357
|
+
// Example 5: CSV Converter Tool Component
|
|
358
|
+
// =============================================================================
|
|
359
|
+
|
|
360
|
+
export function CsvConverterTool() {
|
|
361
|
+
const [inputText, setInputText] = useState('');
|
|
362
|
+
const [outputText, setOutputText] = useState('');
|
|
363
|
+
const [mode, setMode] = useState('csv-to-json'); // or 'json-to-csv'
|
|
364
|
+
const [error, setError] = useState(null);
|
|
365
|
+
const [options, setOptions] = useState({
|
|
366
|
+
delimiter: ',',
|
|
367
|
+
parseNumbers: true,
|
|
368
|
+
parseBooleans: true,
|
|
369
|
+
prettyPrint: true
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
const convert = useCallback(() => {
|
|
373
|
+
setError(null);
|
|
374
|
+
|
|
375
|
+
try {
|
|
376
|
+
if (mode === 'csv-to-json') {
|
|
377
|
+
const data = csvToJson(inputText, {
|
|
378
|
+
delimiter: options.delimiter,
|
|
379
|
+
parseNumbers: options.parseNumbers,
|
|
380
|
+
parseBooleans: options.parseBooleans
|
|
381
|
+
});
|
|
382
|
+
setOutputText(
|
|
383
|
+
options.prettyPrint
|
|
384
|
+
? JSON.stringify(data, null, 2)
|
|
385
|
+
: JSON.stringify(data)
|
|
386
|
+
);
|
|
387
|
+
} else {
|
|
388
|
+
const data = JSON.parse(inputText);
|
|
389
|
+
const csv = jsonToCsv(data, {
|
|
390
|
+
delimiter: options.delimiter
|
|
391
|
+
});
|
|
392
|
+
setOutputText(csv);
|
|
393
|
+
}
|
|
394
|
+
} catch (err) {
|
|
395
|
+
setError(err.message);
|
|
396
|
+
}
|
|
397
|
+
}, [inputText, mode, options]);
|
|
398
|
+
|
|
399
|
+
return (
|
|
400
|
+
<div className="csv-converter">
|
|
401
|
+
<div className="mode-selector">
|
|
402
|
+
<label>
|
|
403
|
+
<input
|
|
404
|
+
type="radio"
|
|
405
|
+
value="csv-to-json"
|
|
406
|
+
checked={mode === 'csv-to-json'}
|
|
407
|
+
onChange={() => setMode('csv-to-json')}
|
|
408
|
+
/>
|
|
409
|
+
CSV to JSON
|
|
410
|
+
</label>
|
|
411
|
+
<label>
|
|
412
|
+
<input
|
|
413
|
+
type="radio"
|
|
414
|
+
value="json-to-csv"
|
|
415
|
+
checked={mode === 'json-to-csv'}
|
|
416
|
+
onChange={() => setMode('json-to-csv')}
|
|
417
|
+
/>
|
|
418
|
+
JSON to CSV
|
|
419
|
+
</label>
|
|
420
|
+
</div>
|
|
421
|
+
|
|
422
|
+
<div className="options">
|
|
423
|
+
<label>
|
|
424
|
+
Delimiter:
|
|
425
|
+
<select
|
|
426
|
+
value={options.delimiter}
|
|
427
|
+
onChange={(e) => setOptions({ ...options, delimiter: e.target.value })}
|
|
428
|
+
>
|
|
429
|
+
<option value=",">Comma (,)</option>
|
|
430
|
+
<option value=";">Semicolon (;)</option>
|
|
431
|
+
<option value="\t">Tab</option>
|
|
432
|
+
<option value="|">Pipe (|)</option>
|
|
433
|
+
</select>
|
|
434
|
+
</label>
|
|
435
|
+
|
|
436
|
+
{mode === 'csv-to-json' && (
|
|
437
|
+
<>
|
|
438
|
+
<label>
|
|
439
|
+
<input
|
|
440
|
+
type="checkbox"
|
|
441
|
+
checked={options.parseNumbers}
|
|
442
|
+
onChange={(e) => setOptions({ ...options, parseNumbers: e.target.checked })}
|
|
443
|
+
/>
|
|
444
|
+
Parse Numbers
|
|
445
|
+
</label>
|
|
446
|
+
<label>
|
|
447
|
+
<input
|
|
448
|
+
type="checkbox"
|
|
449
|
+
checked={options.parseBooleans}
|
|
450
|
+
onChange={(e) => setOptions({ ...options, parseBooleans: e.target.checked })}
|
|
451
|
+
/>
|
|
452
|
+
Parse Booleans
|
|
453
|
+
</label>
|
|
454
|
+
<label>
|
|
455
|
+
<input
|
|
456
|
+
type="checkbox"
|
|
457
|
+
checked={options.prettyPrint}
|
|
458
|
+
onChange={(e) => setOptions({ ...options, prettyPrint: e.target.checked })}
|
|
459
|
+
/>
|
|
460
|
+
Pretty Print
|
|
461
|
+
</label>
|
|
462
|
+
</>
|
|
463
|
+
)}
|
|
464
|
+
</div>
|
|
465
|
+
|
|
466
|
+
<div className="converter-panels">
|
|
467
|
+
<div className="input-panel">
|
|
468
|
+
<h4>Input ({mode === 'csv-to-json' ? 'CSV' : 'JSON'})</h4>
|
|
469
|
+
<textarea
|
|
470
|
+
value={inputText}
|
|
471
|
+
onChange={(e) => setInputText(e.target.value)}
|
|
472
|
+
placeholder={mode === 'csv-to-json'
|
|
473
|
+
? 'Paste CSV here...\nname,age\nJohn,30'
|
|
474
|
+
: 'Paste JSON array here...\n[{"name":"John","age":30}]'
|
|
475
|
+
}
|
|
476
|
+
rows={10}
|
|
477
|
+
/>
|
|
478
|
+
</div>
|
|
479
|
+
|
|
480
|
+
<div className="convert-button">
|
|
481
|
+
<button onClick={convert}>Convert →</button>
|
|
482
|
+
</div>
|
|
483
|
+
|
|
484
|
+
<div className="output-panel">
|
|
485
|
+
<h4>Output ({mode === 'csv-to-json' ? 'JSON' : 'CSV'})</h4>
|
|
486
|
+
<textarea
|
|
487
|
+
value={outputText}
|
|
488
|
+
readOnly
|
|
489
|
+
rows={10}
|
|
490
|
+
/>
|
|
491
|
+
</div>
|
|
492
|
+
</div>
|
|
493
|
+
|
|
494
|
+
{error && (
|
|
495
|
+
<div className="error" style={{ color: 'red' }}>
|
|
496
|
+
Error: {error}
|
|
497
|
+
</div>
|
|
498
|
+
)}
|
|
499
|
+
</div>
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// =============================================================================
|
|
504
|
+
// Example 6: Async Data Loader with CSV Support
|
|
505
|
+
// =============================================================================
|
|
506
|
+
|
|
507
|
+
export function useAsyncCsvLoader(url) {
|
|
508
|
+
const [data, setData] = useState([]);
|
|
509
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
510
|
+
const [error, setError] = useState(null);
|
|
511
|
+
|
|
512
|
+
const load = useCallback(async (parseOptions = {}) => {
|
|
513
|
+
setIsLoading(true);
|
|
514
|
+
setError(null);
|
|
515
|
+
|
|
516
|
+
try {
|
|
517
|
+
const response = await fetch(url);
|
|
518
|
+
if (!response.ok) {
|
|
519
|
+
throw new Error(`HTTP error: ${response.status}`);
|
|
520
|
+
}
|
|
521
|
+
const csvText = await response.text();
|
|
522
|
+
const parsed = csvToJson(csvText, {
|
|
523
|
+
hasHeaders: true,
|
|
524
|
+
trim: true,
|
|
525
|
+
...parseOptions
|
|
526
|
+
});
|
|
527
|
+
setData(parsed);
|
|
528
|
+
return parsed;
|
|
529
|
+
} catch (err) {
|
|
530
|
+
setError(err);
|
|
531
|
+
throw err;
|
|
532
|
+
} finally {
|
|
533
|
+
setIsLoading(false);
|
|
534
|
+
}
|
|
535
|
+
}, [url]);
|
|
536
|
+
|
|
537
|
+
return { data, isLoading, error, load };
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// Example usage component
|
|
541
|
+
export function RemoteCsvLoader({ url }) {
|
|
542
|
+
const { data, isLoading, error, load } = useAsyncCsvLoader(url);
|
|
543
|
+
|
|
544
|
+
React.useEffect(() => {
|
|
545
|
+
load({ parseNumbers: true });
|
|
546
|
+
}, [load]);
|
|
547
|
+
|
|
548
|
+
if (isLoading) return <div>Loading CSV from {url}...</div>;
|
|
549
|
+
if (error) return <div>Error: {error.message}</div>;
|
|
550
|
+
|
|
551
|
+
return (
|
|
552
|
+
<div>
|
|
553
|
+
<h4>Loaded {data.length} rows</h4>
|
|
554
|
+
<pre>{JSON.stringify(data.slice(0, 5), null, 2)}</pre>
|
|
555
|
+
</div>
|
|
556
|
+
);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// =============================================================================
|
|
560
|
+
// Example 7: Complete App Example
|
|
561
|
+
// =============================================================================
|
|
562
|
+
|
|
563
|
+
export function CsvManagerApp() {
|
|
564
|
+
const [activeTab, setActiveTab] = useState('import');
|
|
565
|
+
const [data, setData] = useState([]);
|
|
566
|
+
|
|
567
|
+
return (
|
|
568
|
+
<div className="csv-manager-app">
|
|
569
|
+
<h1>CSV Manager</h1>
|
|
570
|
+
|
|
571
|
+
<div className="tabs">
|
|
572
|
+
<button
|
|
573
|
+
className={activeTab === 'import' ? 'active' : ''}
|
|
574
|
+
onClick={() => setActiveTab('import')}
|
|
575
|
+
>
|
|
576
|
+
Import
|
|
577
|
+
</button>
|
|
578
|
+
<button
|
|
579
|
+
className={activeTab === 'edit' ? 'active' : ''}
|
|
580
|
+
onClick={() => setActiveTab('edit')}
|
|
581
|
+
>
|
|
582
|
+
Edit
|
|
583
|
+
</button>
|
|
584
|
+
<button
|
|
585
|
+
className={activeTab === 'export' ? 'active' : ''}
|
|
586
|
+
onClick={() => setActiveTab('export')}
|
|
587
|
+
>
|
|
588
|
+
Export
|
|
589
|
+
</button>
|
|
590
|
+
<button
|
|
591
|
+
className={activeTab === 'convert' ? 'active' : ''}
|
|
592
|
+
onClick={() => setActiveTab('convert')}
|
|
593
|
+
>
|
|
594
|
+
Convert
|
|
595
|
+
</button>
|
|
596
|
+
</div>
|
|
597
|
+
|
|
598
|
+
<div className="tab-content">
|
|
599
|
+
{activeTab === 'import' && (
|
|
600
|
+
<CsvImporter
|
|
601
|
+
onImport={(imported) => {
|
|
602
|
+
setData(imported);
|
|
603
|
+
setActiveTab('edit');
|
|
604
|
+
}}
|
|
605
|
+
parseOptions={{ parseNumbers: true, parseBooleans: true }}
|
|
606
|
+
/>
|
|
607
|
+
)}
|
|
608
|
+
|
|
609
|
+
{activeTab === 'edit' && (
|
|
610
|
+
<DataTableWithCsv
|
|
611
|
+
initialData={data}
|
|
612
|
+
editable={true}
|
|
613
|
+
onDataChange={setData}
|
|
614
|
+
/>
|
|
615
|
+
)}
|
|
616
|
+
|
|
617
|
+
{activeTab === 'export' && (
|
|
618
|
+
<div>
|
|
619
|
+
<h3>Export Options</h3>
|
|
620
|
+
<CsvExporter
|
|
621
|
+
data={data}
|
|
622
|
+
filename="export.csv"
|
|
623
|
+
options={{ delimiter: ',' }}
|
|
624
|
+
/>
|
|
625
|
+
</div>
|
|
626
|
+
)}
|
|
627
|
+
|
|
628
|
+
{activeTab === 'convert' && (
|
|
629
|
+
<CsvConverterTool />
|
|
630
|
+
)}
|
|
631
|
+
</div>
|
|
632
|
+
</div>
|
|
633
|
+
);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// Default export
|
|
637
|
+
export default CsvManagerApp;
|