@faststore/components 3.98.0-dev.3 → 3.98.0-dev.8
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/dist/cjs/atoms/Link/Link.d.ts +1 -1
- package/dist/cjs/atoms/Link/Link.d.ts.map +1 -1
- package/dist/cjs/atoms/Link/Link.js.map +1 -1
- package/dist/cjs/atoms/List/List.d.ts.map +1 -1
- package/dist/cjs/atoms/List/List.js.map +1 -1
- package/dist/cjs/hooks/index.d.ts +7 -3
- package/dist/cjs/hooks/index.d.ts.map +1 -1
- package/dist/cjs/hooks/index.js +12 -6
- package/dist/cjs/hooks/index.js.map +1 -1
- package/dist/cjs/hooks/useCSVParser.d.ts +43 -0
- package/dist/cjs/hooks/useCSVParser.d.ts.map +1 -0
- package/dist/cjs/hooks/useCSVParser.js +264 -0
- package/dist/cjs/hooks/useCSVParser.js.map +1 -0
- package/dist/cjs/hooks/useFileUpload.d.ts +26 -0
- package/dist/cjs/hooks/useFileUpload.d.ts.map +1 -0
- package/dist/cjs/hooks/useFileUpload.js +68 -0
- package/dist/cjs/hooks/useFileUpload.js.map +1 -0
- package/dist/cjs/index.d.ts +58 -50
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +47 -32
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/molecules/Accordion/AccordionItem.d.ts.map +1 -1
- package/dist/cjs/molecules/Accordion/AccordionItem.js.map +1 -1
- package/dist/cjs/molecules/Dropzone/Dropzone.d.ts +87 -0
- package/dist/cjs/molecules/Dropzone/Dropzone.d.ts.map +1 -0
- package/dist/cjs/molecules/Dropzone/Dropzone.js +85 -0
- package/dist/cjs/molecules/Dropzone/Dropzone.js.map +1 -0
- package/dist/cjs/molecules/Dropzone/index.d.ts +3 -0
- package/dist/cjs/molecules/Dropzone/index.d.ts.map +1 -0
- package/dist/cjs/molecules/Dropzone/index.js +9 -0
- package/dist/cjs/molecules/Dropzone/index.js.map +1 -0
- package/dist/cjs/molecules/FileUploadCard/FileUploadCard.d.ts +113 -0
- package/dist/cjs/molecules/FileUploadCard/FileUploadCard.d.ts.map +1 -0
- package/dist/cjs/molecules/FileUploadCard/FileUploadCard.js +169 -0
- package/dist/cjs/molecules/FileUploadCard/FileUploadCard.js.map +1 -0
- package/dist/cjs/molecules/FileUploadCard/index.d.ts +3 -0
- package/dist/cjs/molecules/FileUploadCard/index.d.ts.map +1 -0
- package/dist/cjs/molecules/FileUploadCard/index.js +9 -0
- package/dist/cjs/molecules/FileUploadCard/index.js.map +1 -0
- package/dist/cjs/molecules/FileUploadStatus/FileUploadStatus.d.ts +93 -0
- package/dist/cjs/molecules/FileUploadStatus/FileUploadStatus.d.ts.map +1 -0
- package/dist/cjs/molecules/FileUploadStatus/FileUploadStatus.js +77 -0
- package/dist/cjs/molecules/FileUploadStatus/FileUploadStatus.js.map +1 -0
- package/dist/cjs/molecules/FileUploadStatus/index.d.ts +3 -0
- package/dist/cjs/molecules/FileUploadStatus/index.d.ts.map +1 -0
- package/dist/cjs/molecules/FileUploadStatus/index.js +11 -0
- package/dist/cjs/molecules/FileUploadStatus/index.js.map +1 -0
- package/dist/cjs/molecules/SearchInputField/SearchInputField.d.ts +23 -1
- package/dist/cjs/molecules/SearchInputField/SearchInputField.d.ts.map +1 -1
- package/dist/cjs/molecules/SearchInputField/SearchInputField.js +9 -4
- package/dist/cjs/molecules/SearchInputField/SearchInputField.js.map +1 -1
- package/dist/cjs/organisms/QuickOrderDrawer/QuickOrderDrawer.d.ts +24 -0
- package/dist/cjs/organisms/QuickOrderDrawer/QuickOrderDrawer.d.ts.map +1 -0
- package/dist/cjs/organisms/QuickOrderDrawer/QuickOrderDrawer.js +14 -0
- package/dist/cjs/organisms/QuickOrderDrawer/QuickOrderDrawer.js.map +1 -0
- package/dist/cjs/organisms/QuickOrderDrawer/QuickOrderDrawerFooter.d.ts +16 -0
- package/dist/cjs/organisms/QuickOrderDrawer/QuickOrderDrawerFooter.d.ts.map +1 -0
- package/dist/cjs/organisms/QuickOrderDrawer/QuickOrderDrawerFooter.js +37 -0
- package/dist/cjs/organisms/QuickOrderDrawer/QuickOrderDrawerFooter.js.map +1 -0
- package/dist/cjs/organisms/QuickOrderDrawer/QuickOrderDrawerHeader.d.ts +9 -0
- package/dist/cjs/organisms/QuickOrderDrawer/QuickOrderDrawerHeader.d.ts.map +1 -0
- package/dist/cjs/organisms/QuickOrderDrawer/QuickOrderDrawerHeader.js +24 -0
- package/dist/cjs/organisms/QuickOrderDrawer/QuickOrderDrawerHeader.js.map +1 -0
- package/dist/cjs/organisms/QuickOrderDrawer/QuickOrderDrawerProducts.d.ts +35 -0
- package/dist/cjs/organisms/QuickOrderDrawer/QuickOrderDrawerProducts.d.ts.map +1 -0
- package/dist/cjs/organisms/QuickOrderDrawer/QuickOrderDrawerProducts.js +91 -0
- package/dist/cjs/organisms/QuickOrderDrawer/QuickOrderDrawerProducts.js.map +1 -0
- package/dist/cjs/organisms/QuickOrderDrawer/index.d.ts +11 -0
- package/dist/cjs/organisms/QuickOrderDrawer/index.d.ts.map +1 -0
- package/dist/cjs/organisms/QuickOrderDrawer/index.js +18 -0
- package/dist/cjs/organisms/QuickOrderDrawer/index.js.map +1 -0
- package/dist/cjs/organisms/QuickOrderDrawer/provider/QuickOrderDrawerProvider.d.ts +61 -0
- package/dist/cjs/organisms/QuickOrderDrawer/provider/QuickOrderDrawerProvider.d.ts.map +1 -0
- package/dist/cjs/organisms/QuickOrderDrawer/provider/QuickOrderDrawerProvider.js +86 -0
- package/dist/cjs/organisms/QuickOrderDrawer/provider/QuickOrderDrawerProvider.js.map +1 -0
- package/dist/esm/atoms/Link/Link.d.ts +1 -1
- package/dist/esm/atoms/Link/Link.d.ts.map +1 -1
- package/dist/esm/atoms/Link/Link.js.map +1 -1
- package/dist/esm/atoms/List/List.d.ts.map +1 -1
- package/dist/esm/atoms/List/List.js.map +1 -1
- package/dist/esm/hooks/index.d.ts +7 -3
- package/dist/esm/hooks/index.d.ts.map +1 -1
- package/dist/esm/hooks/index.js +6 -3
- package/dist/esm/hooks/index.js.map +1 -1
- package/dist/esm/hooks/useCSVParser.d.ts +43 -0
- package/dist/esm/hooks/useCSVParser.d.ts.map +1 -0
- package/dist/esm/hooks/useCSVParser.js +259 -0
- package/dist/esm/hooks/useCSVParser.js.map +1 -0
- package/dist/esm/hooks/useFileUpload.d.ts +26 -0
- package/dist/esm/hooks/useFileUpload.d.ts.map +1 -0
- package/dist/esm/hooks/useFileUpload.js +64 -0
- package/dist/esm/hooks/useFileUpload.js.map +1 -0
- package/dist/esm/index.d.ts +58 -50
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +24 -20
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/molecules/Accordion/AccordionItem.d.ts.map +1 -1
- package/dist/esm/molecules/Accordion/AccordionItem.js.map +1 -1
- package/dist/esm/molecules/Dropzone/Dropzone.d.ts +87 -0
- package/dist/esm/molecules/Dropzone/Dropzone.d.ts.map +1 -0
- package/dist/esm/molecules/Dropzone/Dropzone.js +82 -0
- package/dist/esm/molecules/Dropzone/Dropzone.js.map +1 -0
- package/dist/esm/molecules/Dropzone/index.d.ts +3 -0
- package/dist/esm/molecules/Dropzone/index.d.ts.map +1 -0
- package/dist/esm/molecules/Dropzone/index.js +2 -0
- package/dist/esm/molecules/Dropzone/index.js.map +1 -0
- package/dist/esm/molecules/FileUploadCard/FileUploadCard.d.ts +113 -0
- package/dist/esm/molecules/FileUploadCard/FileUploadCard.d.ts.map +1 -0
- package/dist/esm/molecules/FileUploadCard/FileUploadCard.js +166 -0
- package/dist/esm/molecules/FileUploadCard/FileUploadCard.js.map +1 -0
- package/dist/esm/molecules/FileUploadCard/index.d.ts +3 -0
- package/dist/esm/molecules/FileUploadCard/index.d.ts.map +1 -0
- package/dist/esm/molecules/FileUploadCard/index.js +2 -0
- package/dist/esm/molecules/FileUploadCard/index.js.map +1 -0
- package/dist/esm/molecules/FileUploadStatus/FileUploadStatus.d.ts +93 -0
- package/dist/esm/molecules/FileUploadStatus/FileUploadStatus.d.ts.map +1 -0
- package/dist/esm/molecules/FileUploadStatus/FileUploadStatus.js +73 -0
- package/dist/esm/molecules/FileUploadStatus/FileUploadStatus.js.map +1 -0
- package/dist/esm/molecules/FileUploadStatus/index.d.ts +3 -0
- package/dist/esm/molecules/FileUploadStatus/index.d.ts.map +1 -0
- package/dist/esm/molecules/FileUploadStatus/index.js +2 -0
- package/dist/esm/molecules/FileUploadStatus/index.js.map +1 -0
- package/dist/esm/molecules/SearchInputField/SearchInputField.d.ts +23 -1
- package/dist/esm/molecules/SearchInputField/SearchInputField.d.ts.map +1 -1
- package/dist/esm/molecules/SearchInputField/SearchInputField.js +9 -4
- package/dist/esm/molecules/SearchInputField/SearchInputField.js.map +1 -1
- package/dist/esm/organisms/QuickOrderDrawer/QuickOrderDrawer.d.ts +24 -0
- package/dist/esm/organisms/QuickOrderDrawer/QuickOrderDrawer.d.ts.map +1 -0
- package/dist/esm/organisms/QuickOrderDrawer/QuickOrderDrawer.js +11 -0
- package/dist/esm/organisms/QuickOrderDrawer/QuickOrderDrawer.js.map +1 -0
- package/dist/esm/organisms/QuickOrderDrawer/QuickOrderDrawerFooter.d.ts +16 -0
- package/dist/esm/organisms/QuickOrderDrawer/QuickOrderDrawerFooter.d.ts.map +1 -0
- package/dist/esm/organisms/QuickOrderDrawer/QuickOrderDrawerFooter.js +34 -0
- package/dist/esm/organisms/QuickOrderDrawer/QuickOrderDrawerFooter.js.map +1 -0
- package/dist/esm/organisms/QuickOrderDrawer/QuickOrderDrawerHeader.d.ts +9 -0
- package/dist/esm/organisms/QuickOrderDrawer/QuickOrderDrawerHeader.d.ts.map +1 -0
- package/dist/esm/organisms/QuickOrderDrawer/QuickOrderDrawerHeader.js +21 -0
- package/dist/esm/organisms/QuickOrderDrawer/QuickOrderDrawerHeader.js.map +1 -0
- package/dist/esm/organisms/QuickOrderDrawer/QuickOrderDrawerProducts.d.ts +35 -0
- package/dist/esm/organisms/QuickOrderDrawer/QuickOrderDrawerProducts.d.ts.map +1 -0
- package/dist/esm/organisms/QuickOrderDrawer/QuickOrderDrawerProducts.js +88 -0
- package/dist/esm/organisms/QuickOrderDrawer/QuickOrderDrawerProducts.js.map +1 -0
- package/dist/esm/organisms/QuickOrderDrawer/index.d.ts +11 -0
- package/dist/esm/organisms/QuickOrderDrawer/index.d.ts.map +1 -0
- package/dist/esm/organisms/QuickOrderDrawer/index.js +6 -0
- package/dist/esm/organisms/QuickOrderDrawer/index.js.map +1 -0
- package/dist/esm/organisms/QuickOrderDrawer/provider/QuickOrderDrawerProvider.d.ts +61 -0
- package/dist/esm/organisms/QuickOrderDrawer/provider/QuickOrderDrawerProvider.d.ts.map +1 -0
- package/dist/esm/organisms/QuickOrderDrawer/provider/QuickOrderDrawerProvider.js +80 -0
- package/dist/esm/organisms/QuickOrderDrawer/provider/QuickOrderDrawerProvider.js.map +1 -0
- package/package.json +5 -2
- package/src/atoms/Link/Link.tsx +4 -1
- package/src/atoms/List/List.tsx +18 -16
- package/src/hooks/index.ts +11 -3
- package/src/hooks/useCSVParser.ts +367 -0
- package/src/hooks/useFileUpload.ts +88 -0
- package/src/index.ts +97 -66
- package/src/molecules/Accordion/AccordionItem.tsx +4 -3
- package/src/molecules/Dropzone/Dropzone.tsx +248 -0
- package/src/molecules/Dropzone/index.ts +2 -0
- package/src/molecules/FileUploadCard/FileUploadCard.tsx +406 -0
- package/src/molecules/FileUploadCard/index.tsx +2 -0
- package/src/molecules/FileUploadStatus/FileUploadStatus.tsx +258 -0
- package/src/molecules/FileUploadStatus/index.tsx +6 -0
- package/src/molecules/SearchInputField/SearchInputField.tsx +72 -23
- package/src/organisms/QuickOrderDrawer/QuickOrderDrawer.tsx +58 -0
- package/src/organisms/QuickOrderDrawer/QuickOrderDrawerFooter.tsx +72 -0
- package/src/organisms/QuickOrderDrawer/QuickOrderDrawerHeader.tsx +43 -0
- package/src/organisms/QuickOrderDrawer/QuickOrderDrawerProducts.tsx +323 -0
- package/src/organisms/QuickOrderDrawer/index.ts +19 -0
- 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
|
+
}
|