@faststore/components 3.98.0-dev.3 → 3.98.0

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.
Files changed (171) hide show
  1. package/dist/cjs/atoms/Link/Link.d.ts +1 -1
  2. package/dist/cjs/atoms/Link/Link.d.ts.map +1 -1
  3. package/dist/cjs/atoms/Link/Link.js.map +1 -1
  4. package/dist/cjs/atoms/List/List.d.ts.map +1 -1
  5. package/dist/cjs/atoms/List/List.js.map +1 -1
  6. package/dist/cjs/hooks/index.d.ts +7 -3
  7. package/dist/cjs/hooks/index.d.ts.map +1 -1
  8. package/dist/cjs/hooks/index.js +12 -6
  9. package/dist/cjs/hooks/index.js.map +1 -1
  10. package/dist/cjs/hooks/useCSVParser.d.ts +43 -0
  11. package/dist/cjs/hooks/useCSVParser.d.ts.map +1 -0
  12. package/dist/cjs/hooks/useCSVParser.js +264 -0
  13. package/dist/cjs/hooks/useCSVParser.js.map +1 -0
  14. package/dist/cjs/hooks/useFileUpload.d.ts +26 -0
  15. package/dist/cjs/hooks/useFileUpload.d.ts.map +1 -0
  16. package/dist/cjs/hooks/useFileUpload.js +68 -0
  17. package/dist/cjs/hooks/useFileUpload.js.map +1 -0
  18. package/dist/cjs/index.d.ts +58 -50
  19. package/dist/cjs/index.d.ts.map +1 -1
  20. package/dist/cjs/index.js +47 -32
  21. package/dist/cjs/index.js.map +1 -1
  22. package/dist/cjs/molecules/Accordion/AccordionItem.d.ts.map +1 -1
  23. package/dist/cjs/molecules/Accordion/AccordionItem.js.map +1 -1
  24. package/dist/cjs/molecules/Dropzone/Dropzone.d.ts +87 -0
  25. package/dist/cjs/molecules/Dropzone/Dropzone.d.ts.map +1 -0
  26. package/dist/cjs/molecules/Dropzone/Dropzone.js +85 -0
  27. package/dist/cjs/molecules/Dropzone/Dropzone.js.map +1 -0
  28. package/dist/cjs/molecules/Dropzone/index.d.ts +3 -0
  29. package/dist/cjs/molecules/Dropzone/index.d.ts.map +1 -0
  30. package/dist/cjs/molecules/Dropzone/index.js +9 -0
  31. package/dist/cjs/molecules/Dropzone/index.js.map +1 -0
  32. package/dist/cjs/molecules/FileUploadCard/FileUploadCard.d.ts +113 -0
  33. package/dist/cjs/molecules/FileUploadCard/FileUploadCard.d.ts.map +1 -0
  34. package/dist/cjs/molecules/FileUploadCard/FileUploadCard.js +169 -0
  35. package/dist/cjs/molecules/FileUploadCard/FileUploadCard.js.map +1 -0
  36. package/dist/cjs/molecules/FileUploadCard/index.d.ts +3 -0
  37. package/dist/cjs/molecules/FileUploadCard/index.d.ts.map +1 -0
  38. package/dist/cjs/molecules/FileUploadCard/index.js +9 -0
  39. package/dist/cjs/molecules/FileUploadCard/index.js.map +1 -0
  40. package/dist/cjs/molecules/FileUploadStatus/FileUploadStatus.d.ts +93 -0
  41. package/dist/cjs/molecules/FileUploadStatus/FileUploadStatus.d.ts.map +1 -0
  42. package/dist/cjs/molecules/FileUploadStatus/FileUploadStatus.js +77 -0
  43. package/dist/cjs/molecules/FileUploadStatus/FileUploadStatus.js.map +1 -0
  44. package/dist/cjs/molecules/FileUploadStatus/index.d.ts +3 -0
  45. package/dist/cjs/molecules/FileUploadStatus/index.d.ts.map +1 -0
  46. package/dist/cjs/molecules/FileUploadStatus/index.js +11 -0
  47. package/dist/cjs/molecules/FileUploadStatus/index.js.map +1 -0
  48. package/dist/cjs/molecules/SearchInputField/SearchInputField.d.ts +23 -1
  49. package/dist/cjs/molecules/SearchInputField/SearchInputField.d.ts.map +1 -1
  50. package/dist/cjs/molecules/SearchInputField/SearchInputField.js +9 -4
  51. package/dist/cjs/molecules/SearchInputField/SearchInputField.js.map +1 -1
  52. package/dist/cjs/organisms/QuickOrderDrawer/QuickOrderDrawer.d.ts +24 -0
  53. package/dist/cjs/organisms/QuickOrderDrawer/QuickOrderDrawer.d.ts.map +1 -0
  54. package/dist/cjs/organisms/QuickOrderDrawer/QuickOrderDrawer.js +14 -0
  55. package/dist/cjs/organisms/QuickOrderDrawer/QuickOrderDrawer.js.map +1 -0
  56. package/dist/cjs/organisms/QuickOrderDrawer/QuickOrderDrawerFooter.d.ts +16 -0
  57. package/dist/cjs/organisms/QuickOrderDrawer/QuickOrderDrawerFooter.d.ts.map +1 -0
  58. package/dist/cjs/organisms/QuickOrderDrawer/QuickOrderDrawerFooter.js +37 -0
  59. package/dist/cjs/organisms/QuickOrderDrawer/QuickOrderDrawerFooter.js.map +1 -0
  60. package/dist/cjs/organisms/QuickOrderDrawer/QuickOrderDrawerHeader.d.ts +9 -0
  61. package/dist/cjs/organisms/QuickOrderDrawer/QuickOrderDrawerHeader.d.ts.map +1 -0
  62. package/dist/cjs/organisms/QuickOrderDrawer/QuickOrderDrawerHeader.js +24 -0
  63. package/dist/cjs/organisms/QuickOrderDrawer/QuickOrderDrawerHeader.js.map +1 -0
  64. package/dist/cjs/organisms/QuickOrderDrawer/QuickOrderDrawerProducts.d.ts +35 -0
  65. package/dist/cjs/organisms/QuickOrderDrawer/QuickOrderDrawerProducts.d.ts.map +1 -0
  66. package/dist/cjs/organisms/QuickOrderDrawer/QuickOrderDrawerProducts.js +91 -0
  67. package/dist/cjs/organisms/QuickOrderDrawer/QuickOrderDrawerProducts.js.map +1 -0
  68. package/dist/cjs/organisms/QuickOrderDrawer/index.d.ts +11 -0
  69. package/dist/cjs/organisms/QuickOrderDrawer/index.d.ts.map +1 -0
  70. package/dist/cjs/organisms/QuickOrderDrawer/index.js +18 -0
  71. package/dist/cjs/organisms/QuickOrderDrawer/index.js.map +1 -0
  72. package/dist/cjs/organisms/QuickOrderDrawer/provider/QuickOrderDrawerProvider.d.ts +61 -0
  73. package/dist/cjs/organisms/QuickOrderDrawer/provider/QuickOrderDrawerProvider.d.ts.map +1 -0
  74. package/dist/cjs/organisms/QuickOrderDrawer/provider/QuickOrderDrawerProvider.js +86 -0
  75. package/dist/cjs/organisms/QuickOrderDrawer/provider/QuickOrderDrawerProvider.js.map +1 -0
  76. package/dist/esm/atoms/Link/Link.d.ts +1 -1
  77. package/dist/esm/atoms/Link/Link.d.ts.map +1 -1
  78. package/dist/esm/atoms/Link/Link.js.map +1 -1
  79. package/dist/esm/atoms/List/List.d.ts.map +1 -1
  80. package/dist/esm/atoms/List/List.js.map +1 -1
  81. package/dist/esm/hooks/index.d.ts +7 -3
  82. package/dist/esm/hooks/index.d.ts.map +1 -1
  83. package/dist/esm/hooks/index.js +6 -3
  84. package/dist/esm/hooks/index.js.map +1 -1
  85. package/dist/esm/hooks/useCSVParser.d.ts +43 -0
  86. package/dist/esm/hooks/useCSVParser.d.ts.map +1 -0
  87. package/dist/esm/hooks/useCSVParser.js +259 -0
  88. package/dist/esm/hooks/useCSVParser.js.map +1 -0
  89. package/dist/esm/hooks/useFileUpload.d.ts +26 -0
  90. package/dist/esm/hooks/useFileUpload.d.ts.map +1 -0
  91. package/dist/esm/hooks/useFileUpload.js +64 -0
  92. package/dist/esm/hooks/useFileUpload.js.map +1 -0
  93. package/dist/esm/index.d.ts +58 -50
  94. package/dist/esm/index.d.ts.map +1 -1
  95. package/dist/esm/index.js +24 -20
  96. package/dist/esm/index.js.map +1 -1
  97. package/dist/esm/molecules/Accordion/AccordionItem.d.ts.map +1 -1
  98. package/dist/esm/molecules/Accordion/AccordionItem.js.map +1 -1
  99. package/dist/esm/molecules/Dropzone/Dropzone.d.ts +87 -0
  100. package/dist/esm/molecules/Dropzone/Dropzone.d.ts.map +1 -0
  101. package/dist/esm/molecules/Dropzone/Dropzone.js +82 -0
  102. package/dist/esm/molecules/Dropzone/Dropzone.js.map +1 -0
  103. package/dist/esm/molecules/Dropzone/index.d.ts +3 -0
  104. package/dist/esm/molecules/Dropzone/index.d.ts.map +1 -0
  105. package/dist/esm/molecules/Dropzone/index.js +2 -0
  106. package/dist/esm/molecules/Dropzone/index.js.map +1 -0
  107. package/dist/esm/molecules/FileUploadCard/FileUploadCard.d.ts +113 -0
  108. package/dist/esm/molecules/FileUploadCard/FileUploadCard.d.ts.map +1 -0
  109. package/dist/esm/molecules/FileUploadCard/FileUploadCard.js +166 -0
  110. package/dist/esm/molecules/FileUploadCard/FileUploadCard.js.map +1 -0
  111. package/dist/esm/molecules/FileUploadCard/index.d.ts +3 -0
  112. package/dist/esm/molecules/FileUploadCard/index.d.ts.map +1 -0
  113. package/dist/esm/molecules/FileUploadCard/index.js +2 -0
  114. package/dist/esm/molecules/FileUploadCard/index.js.map +1 -0
  115. package/dist/esm/molecules/FileUploadStatus/FileUploadStatus.d.ts +93 -0
  116. package/dist/esm/molecules/FileUploadStatus/FileUploadStatus.d.ts.map +1 -0
  117. package/dist/esm/molecules/FileUploadStatus/FileUploadStatus.js +73 -0
  118. package/dist/esm/molecules/FileUploadStatus/FileUploadStatus.js.map +1 -0
  119. package/dist/esm/molecules/FileUploadStatus/index.d.ts +3 -0
  120. package/dist/esm/molecules/FileUploadStatus/index.d.ts.map +1 -0
  121. package/dist/esm/molecules/FileUploadStatus/index.js +2 -0
  122. package/dist/esm/molecules/FileUploadStatus/index.js.map +1 -0
  123. package/dist/esm/molecules/SearchInputField/SearchInputField.d.ts +23 -1
  124. package/dist/esm/molecules/SearchInputField/SearchInputField.d.ts.map +1 -1
  125. package/dist/esm/molecules/SearchInputField/SearchInputField.js +9 -4
  126. package/dist/esm/molecules/SearchInputField/SearchInputField.js.map +1 -1
  127. package/dist/esm/organisms/QuickOrderDrawer/QuickOrderDrawer.d.ts +24 -0
  128. package/dist/esm/organisms/QuickOrderDrawer/QuickOrderDrawer.d.ts.map +1 -0
  129. package/dist/esm/organisms/QuickOrderDrawer/QuickOrderDrawer.js +11 -0
  130. package/dist/esm/organisms/QuickOrderDrawer/QuickOrderDrawer.js.map +1 -0
  131. package/dist/esm/organisms/QuickOrderDrawer/QuickOrderDrawerFooter.d.ts +16 -0
  132. package/dist/esm/organisms/QuickOrderDrawer/QuickOrderDrawerFooter.d.ts.map +1 -0
  133. package/dist/esm/organisms/QuickOrderDrawer/QuickOrderDrawerFooter.js +34 -0
  134. package/dist/esm/organisms/QuickOrderDrawer/QuickOrderDrawerFooter.js.map +1 -0
  135. package/dist/esm/organisms/QuickOrderDrawer/QuickOrderDrawerHeader.d.ts +9 -0
  136. package/dist/esm/organisms/QuickOrderDrawer/QuickOrderDrawerHeader.d.ts.map +1 -0
  137. package/dist/esm/organisms/QuickOrderDrawer/QuickOrderDrawerHeader.js +21 -0
  138. package/dist/esm/organisms/QuickOrderDrawer/QuickOrderDrawerHeader.js.map +1 -0
  139. package/dist/esm/organisms/QuickOrderDrawer/QuickOrderDrawerProducts.d.ts +35 -0
  140. package/dist/esm/organisms/QuickOrderDrawer/QuickOrderDrawerProducts.d.ts.map +1 -0
  141. package/dist/esm/organisms/QuickOrderDrawer/QuickOrderDrawerProducts.js +88 -0
  142. package/dist/esm/organisms/QuickOrderDrawer/QuickOrderDrawerProducts.js.map +1 -0
  143. package/dist/esm/organisms/QuickOrderDrawer/index.d.ts +11 -0
  144. package/dist/esm/organisms/QuickOrderDrawer/index.d.ts.map +1 -0
  145. package/dist/esm/organisms/QuickOrderDrawer/index.js +6 -0
  146. package/dist/esm/organisms/QuickOrderDrawer/index.js.map +1 -0
  147. package/dist/esm/organisms/QuickOrderDrawer/provider/QuickOrderDrawerProvider.d.ts +61 -0
  148. package/dist/esm/organisms/QuickOrderDrawer/provider/QuickOrderDrawerProvider.d.ts.map +1 -0
  149. package/dist/esm/organisms/QuickOrderDrawer/provider/QuickOrderDrawerProvider.js +80 -0
  150. package/dist/esm/organisms/QuickOrderDrawer/provider/QuickOrderDrawerProvider.js.map +1 -0
  151. package/package.json +5 -2
  152. package/src/atoms/Link/Link.tsx +4 -1
  153. package/src/atoms/List/List.tsx +18 -16
  154. package/src/hooks/index.ts +11 -3
  155. package/src/hooks/useCSVParser.ts +367 -0
  156. package/src/hooks/useFileUpload.ts +88 -0
  157. package/src/index.ts +97 -66
  158. package/src/molecules/Accordion/AccordionItem.tsx +4 -3
  159. package/src/molecules/Dropzone/Dropzone.tsx +248 -0
  160. package/src/molecules/Dropzone/index.ts +2 -0
  161. package/src/molecules/FileUploadCard/FileUploadCard.tsx +406 -0
  162. package/src/molecules/FileUploadCard/index.tsx +2 -0
  163. package/src/molecules/FileUploadStatus/FileUploadStatus.tsx +258 -0
  164. package/src/molecules/FileUploadStatus/index.tsx +6 -0
  165. package/src/molecules/SearchInputField/SearchInputField.tsx +72 -23
  166. package/src/organisms/QuickOrderDrawer/QuickOrderDrawer.tsx +58 -0
  167. package/src/organisms/QuickOrderDrawer/QuickOrderDrawerFooter.tsx +72 -0
  168. package/src/organisms/QuickOrderDrawer/QuickOrderDrawerHeader.tsx +43 -0
  169. package/src/organisms/QuickOrderDrawer/QuickOrderDrawerProducts.tsx +323 -0
  170. package/src/organisms/QuickOrderDrawer/index.ts +19 -0
  171. package/src/organisms/QuickOrderDrawer/provider/QuickOrderDrawerProvider.tsx +205 -0
@@ -0,0 +1,367 @@
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
+ }
@@ -0,0 +1,88 @@
1
+ import { useCallback, useState } from 'react'
2
+ import type { FileRejection } from 'react-dropzone'
3
+
4
+ /**
5
+ * Error codes returned by react-dropzone when a file is rejected.
6
+ */
7
+ export enum FileRejectionCode {
8
+ FileTooLarge = 'file-too-large',
9
+ FileInvalidType = 'file-invalid-type',
10
+ TooManyFiles = 'too-many-files',
11
+ }
12
+
13
+ export type FileUploadOptions = {
14
+ maxFiles?: number
15
+ maxSize?: number
16
+ acceptedTypes?: Record<string, string[]>
17
+ }
18
+
19
+ const MAX_FILES_DEFAULT = 1
20
+ const MAX_SIZE_DEFAULT = 100 * 1024 * 1024 // 100MB
21
+ const ACCEPTED_TYPES_DEFAULT = {
22
+ 'text/csv': ['.csv'],
23
+ }
24
+
25
+ export const DEFAULT_FILE_UPLOAD_OPTIONS: Required<FileUploadOptions> = {
26
+ maxFiles: MAX_FILES_DEFAULT,
27
+ maxSize: MAX_SIZE_DEFAULT,
28
+ acceptedTypes: ACCEPTED_TYPES_DEFAULT,
29
+ }
30
+
31
+ /**
32
+ * Hook to manage file upload errors and rejections.
33
+ * @param options Configuration options for file upload.
34
+ * @returns An object containing upload error state and handlers.
35
+ */
36
+ export function useFileUpload(options: FileUploadOptions = {}) {
37
+ const resolvedOptions = { ...DEFAULT_FILE_UPLOAD_OPTIONS, ...options }
38
+ const { maxFiles, maxSize, acceptedTypes } = resolvedOptions
39
+ const allowedExtensions = Object.values(acceptedTypes)
40
+ .flat()
41
+ .map((ext) => ext.replace('.', '').toUpperCase())
42
+ .join(', ')
43
+
44
+ const [uploadError, setUploadError] = useState<string | null>(null)
45
+
46
+ const getErrorMessage = useCallback(
47
+ (code: string) => {
48
+ switch (code) {
49
+ case FileRejectionCode.FileTooLarge: {
50
+ const sizeMB = Math.round(maxSize / (1024 * 1024))
51
+ return `File is too large. Maximum size is ${sizeMB}MB.`
52
+ }
53
+
54
+ case FileRejectionCode.FileInvalidType:
55
+ return `Invalid file type. Please upload a ${allowedExtensions} file.`
56
+
57
+ case FileRejectionCode.TooManyFiles:
58
+ return `Too many files. Please upload only ${maxFiles} file(s).`
59
+
60
+ default:
61
+ return 'Failed to upload file. Please try again.'
62
+ }
63
+ },
64
+ [maxFiles, maxSize, allowedExtensions]
65
+ )
66
+
67
+ const onFilesRejected = useCallback(
68
+ (fileRejections: FileRejection[]) => {
69
+ const firstError = fileRejections[0]?.errors[0]
70
+
71
+ if (firstError) {
72
+ const errorMessage = getErrorMessage(firstError.code)
73
+ setUploadError(errorMessage)
74
+ }
75
+ },
76
+ [getErrorMessage]
77
+ )
78
+
79
+ const onClearUploadError = useCallback(() => {
80
+ setUploadError(null)
81
+ }, [])
82
+
83
+ return {
84
+ uploadError,
85
+ onFilesRejected,
86
+ onClearUploadError,
87
+ }
88
+ }