@faststore/components 4.3.0-dev.0 → 4.3.0-dev.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.
@@ -1,367 +0,0 @@
1
- import Papa from 'papaparse'
2
- import { useCallback, useMemo, useState } from 'react'
3
-
4
- export interface WorkerCSVData {
5
- data: Array<{ sku: string; quantity: number }>
6
- fileName: string
7
- totalRows: number
8
- fileSize: number
9
- }
10
-
11
- export interface WorkerCSVOptions {
12
- skuColumnNames?: string[]
13
- quantityColumnNames?: string[]
14
- delimiter?: string
15
- skipEmptyLines?: boolean
16
- chunkSize?: number
17
- onProgress?: (progress: {
18
- processed: number
19
- total: number
20
- percentage: number
21
- }) => void
22
- }
23
-
24
- export type CSVData = WorkerCSVData
25
- export type CSVParserOptions = WorkerCSVOptions
26
-
27
- export type CSVParserError = {
28
- message: string
29
- type: 'PARSE_ERROR' | 'VALIDATION_ERROR' | 'FILE_ERROR'
30
- }
31
-
32
- const defaultOptions: CSVParserOptions = {}
33
-
34
- /**
35
- * Hook to parse CSV files containing SKU and Quantity columns.
36
- * Utilizes PapaParse's native Web Worker for efficient parsing of large files.
37
- * Supports both comma (,) and semicolon (;) delimiters - automatically detects which one is used.
38
- * @param options CSV parsing options
39
- * @returns Object containing parsing state and functions
40
- */
41
- export function useCSVParser(options?: CSVParserOptions) {
42
- const [error, setError] = useState<CSVParserError | null>(null)
43
- const [isParsing, setIsParsing] = useState(false)
44
- const [isGeneratingTemplate, setIsGeneratingTemplate] = useState(false)
45
-
46
- const mergedOptions = useMemo(
47
- () => ({ ...defaultOptions, ...options }),
48
- [options]
49
- )
50
-
51
- const onParseFile = useCallback(
52
- async (file: File): Promise<CSVData | null> => {
53
- try {
54
- setError(null)
55
- setIsParsing(true)
56
-
57
- const result = await parseCSVFile(file, mergedOptions)
58
- return result
59
- } catch (err) {
60
- const error: CSVParserError = {
61
- message:
62
- err instanceof Error ? err.message : 'Failed to parse CSV file',
63
- type: 'PARSE_ERROR',
64
- }
65
- setError(error)
66
- return null
67
- } finally {
68
- setIsParsing(false)
69
- }
70
- },
71
- [mergedOptions]
72
- )
73
-
74
- const onGenerateTemplate = useCallback(async (): Promise<string | null> => {
75
- try {
76
- setIsGeneratingTemplate(true)
77
- const csvContent = generateCSVTemplate()
78
- return csvContent
79
- } catch (err) {
80
- const error: CSVParserError = {
81
- message:
82
- err instanceof Error ? err.message : 'Failed to generate template',
83
- type: 'PARSE_ERROR',
84
- }
85
- setError(error)
86
- return null
87
- } finally {
88
- setIsGeneratingTemplate(false)
89
- }
90
- }, [])
91
-
92
- const onClearError = useCallback(() => {
93
- setError(null)
94
- }, [])
95
-
96
- return {
97
- error,
98
- isParsing,
99
- isGeneratingTemplate,
100
- onParseFile,
101
- onGenerateTemplate,
102
- onClearError,
103
- }
104
- }
105
-
106
- /**
107
- * Parse CSV file using PapaParse's native Web Worker
108
- */
109
- const parseCSVFile = (
110
- file: File,
111
- options: WorkerCSVOptions = {}
112
- ): Promise<WorkerCSVData> => {
113
- return new Promise(async (resolve, reject) => {
114
- const defaultOptions = {
115
- skuColumnNames: ['sku', 'id', 'product', 'productid', 'item'],
116
- quantityColumnNames: ['quantity', 'qty', 'amount', 'count'],
117
- delimiter: '',
118
- skipEmptyLines: true,
119
- chunkSize: 1024 * 1024,
120
- }
121
-
122
- const config = { ...defaultOptions, ...options }
123
-
124
- const detectDelimiter = (): Promise<string> => {
125
- if (config.delimiter) {
126
- return Promise.resolve(config.delimiter)
127
- }
128
-
129
- return new Promise((resolveDelimiter) => {
130
- const reader = new FileReader()
131
- reader.onload = (e) => {
132
- const text = e.target?.result as string
133
- const firstLine = (text.split(/\r?\n/)[0] || '').replace(/\r$/, '')
134
-
135
- const commaCount = (firstLine.match(/,/g) || []).length
136
- const semicolonCount = (firstLine.match(/;/g) || []).length
137
- const tabCount = (firstLine.match(/\t/g) || []).length
138
-
139
- if (semicolonCount > 0 && semicolonCount >= commaCount) {
140
- resolveDelimiter(';')
141
- } else if (tabCount > commaCount && tabCount > semicolonCount) {
142
- resolveDelimiter('\t')
143
- } else {
144
- resolveDelimiter(',')
145
- }
146
- }
147
- reader.onerror = () => resolveDelimiter(',')
148
- reader.readAsText(file.slice(0, 1024))
149
- })
150
- }
151
-
152
- const detectedDelimiter = await detectDelimiter()
153
-
154
- let headers: string[] = []
155
- let skuIndex = -1
156
- let quantityIndex = -1
157
- let isHeaderProcessed = false
158
-
159
- const transformedData: Array<{ sku: string; quantity: number }> = []
160
- const errors: string[] = []
161
- let processedRows = 0
162
- let totalEstimatedRows = 0
163
-
164
- const findColumnIndex = (
165
- headers: string[],
166
- columnNames: string[]
167
- ): number => {
168
- return headers.findIndex((header) =>
169
- columnNames.some(
170
- (name) => header.toLowerCase().trim() === name.toLowerCase()
171
- )
172
- )
173
- }
174
-
175
- const validateAndTransformRow = (row: unknown[], rowIndex: number) => {
176
- try {
177
- const sku = row[skuIndex]
178
- const quantity = row[quantityIndex]
179
-
180
- if (!sku || sku === '' || quantity === undefined || quantity === '') {
181
- return null
182
- }
183
-
184
- const trimmedSku = String(sku).trim()
185
- const numericQuantity = Number(quantity)
186
-
187
- if (!trimmedSku) {
188
- throw new Error('Empty SKU found')
189
- }
190
-
191
- if (Number.isNaN(numericQuantity) || numericQuantity < 0) {
192
- throw new Error(
193
- `Invalid quantity value: ${quantity} for SKU: ${trimmedSku}`
194
- )
195
- }
196
-
197
- return {
198
- sku: trimmedSku,
199
- quantity: numericQuantity,
200
- }
201
- } catch (err) {
202
- const errorMessage =
203
- err instanceof Error ? err.message : 'Unknown error'
204
- errors.push(`Row ${rowIndex + 2}: ${errorMessage}`)
205
-
206
- if (errors.length > 1000) {
207
- errors.splice(0, 500)
208
- }
209
-
210
- return null
211
- }
212
- }
213
-
214
- const estimateRows = (fileSize: number) => {
215
- const avgBytesPerRow = 50
216
- return Math.floor(fileSize / avgBytesPerRow)
217
- }
218
-
219
- totalEstimatedRows = estimateRows(file.size)
220
-
221
- Papa.parse(file, {
222
- header: false,
223
- dynamicTyping: false,
224
- skipEmptyLines: config.skipEmptyLines,
225
- delimiter: detectedDelimiter,
226
-
227
- worker: true,
228
-
229
- chunk: (
230
- results: { data: unknown[][] },
231
- parser: { abort: () => void }
232
- ) => {
233
- try {
234
- let rows = results.data
235
-
236
- if (!isHeaderProcessed && rows.length > 0) {
237
- headers = rows[0] as string[]
238
-
239
- skuIndex = findColumnIndex(headers, config.skuColumnNames)
240
- quantityIndex = findColumnIndex(headers, config.quantityColumnNames)
241
-
242
- if (skuIndex === -1) {
243
- parser.abort()
244
- reject(
245
- new Error(
246
- `SKU column not found. Expected one of: ${config.skuColumnNames.join(', ')}`
247
- )
248
- )
249
- return
250
- }
251
-
252
- if (quantityIndex === -1) {
253
- parser.abort()
254
- reject(
255
- new Error(
256
- `Quantity column not found. Expected one of: ${config.quantityColumnNames.join(', ')}`
257
- )
258
- )
259
- return
260
- }
261
-
262
- rows = rows.slice(1)
263
- isHeaderProcessed = true
264
- }
265
-
266
- rows.forEach((row, index) => {
267
- const globalRowIndex = processedRows + index
268
- const transformedRow = validateAndTransformRow(row, globalRowIndex)
269
-
270
- if (transformedRow) {
271
- transformedData.push(transformedRow)
272
- }
273
- })
274
-
275
- processedRows += rows.length
276
-
277
- if (config.onProgress) {
278
- const percentage = Math.min(
279
- 100,
280
- Math.round((processedRows / totalEstimatedRows) * 100)
281
- )
282
- config.onProgress({
283
- processed: processedRows,
284
- total: totalEstimatedRows,
285
- percentage,
286
- })
287
- }
288
- } catch (err) {
289
- parser.abort()
290
- reject(err)
291
- }
292
- },
293
-
294
- complete: () => {
295
- try {
296
- if (transformedData.length === 0) {
297
- if (errors.length > 0) {
298
- reject(
299
- new Error(
300
- `No valid data found. First few errors:\n${errors.slice(0, 5).join('\n')}`
301
- )
302
- )
303
- } else {
304
- reject(new Error('No valid data found in file'))
305
- }
306
- return
307
- }
308
-
309
- if (errors.length > 0) {
310
- console.warn(
311
- `CSV parsing completed with ${errors.length} warnings. Sample:`,
312
- errors.slice(0, 10)
313
- )
314
- }
315
-
316
- if (config.onProgress) {
317
- config.onProgress({
318
- processed: processedRows,
319
- total: processedRows,
320
- percentage: 100,
321
- })
322
- }
323
-
324
- resolve({
325
- data: transformedData,
326
- fileName: file.name,
327
- totalRows: transformedData.length,
328
- fileSize: file.size,
329
- })
330
- } catch (err) {
331
- reject(err)
332
- }
333
- },
334
-
335
- error: (error: unknown) => {
336
- const errorMessage =
337
- error instanceof Error ? error.message : 'Unknown parsing error'
338
- reject(new Error(`PapaParse error: ${errorMessage}`))
339
- },
340
-
341
- fastMode: false,
342
- chunkSize: config.chunkSize,
343
- preview: 0,
344
- encoding: 'UTF-8',
345
- })
346
- })
347
- }
348
-
349
- /**
350
- * Generate CSV template with sample data
351
- */
352
- const generateCSVTemplate = (): string => {
353
- const templateData = [
354
- { sku: 'PROD-001', quantity: 5 },
355
- { sku: 'ITEM-234', quantity: 12 },
356
- { sku: 'SKU789', quantity: 3 },
357
- { sku: 'ABC-XYZ-456', quantity: 8 },
358
- { sku: 'SAMPLE-100', quantity: 25 },
359
- ]
360
-
361
- return Papa.unparse(templateData, {
362
- header: true,
363
- delimiter: ',',
364
- newline: '\n',
365
- skipEmptyLines: true,
366
- })
367
- }