@igorvaryvoda/sirv-upload-widget 0.1.0 → 0.1.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/dist/index.d.mts +574 -0
- package/dist/index.d.ts +574 -0
- package/dist/index.js +1787 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +1759 -0
- package/dist/index.mjs.map +1 -0
- package/dist/styles.css +806 -0
- package/package.json +1 -1
package/dist/index.js
ADDED
|
@@ -0,0 +1,1787 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var react = require('react');
|
|
4
|
+
var clsx2 = require('clsx');
|
|
5
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
6
|
+
|
|
7
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
8
|
+
|
|
9
|
+
var clsx2__default = /*#__PURE__*/_interopDefault(clsx2);
|
|
10
|
+
|
|
11
|
+
// src/components/SirvUploader.tsx
|
|
12
|
+
|
|
13
|
+
// src/utils/image-utils.ts
|
|
14
|
+
var HEIC_TYPES = ["image/heic", "image/heif"];
|
|
15
|
+
var IMAGE_EXTENSIONS = /\.(jpe?g|png|gif|webp|heic|heif|bmp|tiff?|avif)$/i;
|
|
16
|
+
var ACCEPTED_IMAGE_FORMATS = "image/jpeg,image/png,image/gif,image/webp,image/bmp,image/tiff,image/heic,image/heif,image/avif,.jpg,.jpeg,.png,.gif,.webp,.bmp,.tif,.tiff,.heic,.heif,.avif";
|
|
17
|
+
var DEFAULT_MAX_FILE_SIZE = 10 * 1024 * 1024;
|
|
18
|
+
function isImageFile(file) {
|
|
19
|
+
if (file.type.startsWith("image/")) return true;
|
|
20
|
+
if (IMAGE_EXTENSIONS.test(file.name)) return true;
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
function isHeifFile(file) {
|
|
24
|
+
if (HEIC_TYPES.includes(file.type.toLowerCase())) return true;
|
|
25
|
+
if (/\.(heic|heif)$/i.test(file.name)) return true;
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
async function convertHeicToJpeg(file) {
|
|
29
|
+
const heic2any = (await import('heic2any')).default;
|
|
30
|
+
const blob = await heic2any({
|
|
31
|
+
blob: file,
|
|
32
|
+
toType: "image/jpeg",
|
|
33
|
+
quality: 0.92
|
|
34
|
+
});
|
|
35
|
+
const resultBlob = Array.isArray(blob) ? blob[0] : blob;
|
|
36
|
+
if (!resultBlob || resultBlob.size === 0) {
|
|
37
|
+
throw new Error("HEIC conversion produced empty result");
|
|
38
|
+
}
|
|
39
|
+
const newName = file.name.replace(/\.(heic|heif)$/i, ".jpg") || "converted.jpg";
|
|
40
|
+
return new File([resultBlob], newName.endsWith(".jpg") ? newName : `${newName}.jpg`, {
|
|
41
|
+
type: "image/jpeg"
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
async function convertHeicWithFallback(file, serverEndpoint) {
|
|
45
|
+
try {
|
|
46
|
+
return await convertHeicToJpeg(file);
|
|
47
|
+
} catch (primaryError) {
|
|
48
|
+
console.warn("Primary HEIC conversion (heic2any) failed:", primaryError);
|
|
49
|
+
try {
|
|
50
|
+
const img = new Image();
|
|
51
|
+
const canvas = document.createElement("canvas");
|
|
52
|
+
const ctx = canvas.getContext("2d");
|
|
53
|
+
if (!ctx) throw new Error("Canvas not supported");
|
|
54
|
+
const objectUrl = URL.createObjectURL(file);
|
|
55
|
+
await new Promise((resolve, reject) => {
|
|
56
|
+
img.onload = () => resolve();
|
|
57
|
+
img.onerror = () => reject(new Error("Browser cannot decode HEIC natively"));
|
|
58
|
+
img.src = objectUrl;
|
|
59
|
+
});
|
|
60
|
+
canvas.width = img.naturalWidth;
|
|
61
|
+
canvas.height = img.naturalHeight;
|
|
62
|
+
ctx.drawImage(img, 0, 0);
|
|
63
|
+
URL.revokeObjectURL(objectUrl);
|
|
64
|
+
const blob = await new Promise((resolve) => {
|
|
65
|
+
canvas.toBlob(resolve, "image/jpeg", 0.92);
|
|
66
|
+
});
|
|
67
|
+
if (!blob || blob.size === 0) {
|
|
68
|
+
throw new Error("Canvas conversion produced empty result");
|
|
69
|
+
}
|
|
70
|
+
const newName = file.name.replace(/\.(heic|heif)$/i, ".jpg") || "converted.jpg";
|
|
71
|
+
return new File([blob], newName.endsWith(".jpg") ? newName : `${newName}.jpg`, {
|
|
72
|
+
type: "image/jpeg"
|
|
73
|
+
});
|
|
74
|
+
} catch (canvasError) {
|
|
75
|
+
console.warn("Canvas fallback failed:", canvasError);
|
|
76
|
+
if (serverEndpoint) {
|
|
77
|
+
try {
|
|
78
|
+
const formData = new FormData();
|
|
79
|
+
formData.append("file", file);
|
|
80
|
+
const response = await fetch(serverEndpoint, {
|
|
81
|
+
method: "POST",
|
|
82
|
+
body: formData
|
|
83
|
+
});
|
|
84
|
+
if (!response.ok) {
|
|
85
|
+
throw new Error(`Server returned ${response.status}`);
|
|
86
|
+
}
|
|
87
|
+
const { dataUrl, filename } = await response.json();
|
|
88
|
+
const base64Data = dataUrl.split(",")[1];
|
|
89
|
+
const binaryString = atob(base64Data);
|
|
90
|
+
const bytes = new Uint8Array(binaryString.length);
|
|
91
|
+
for (let i = 0; i < binaryString.length; i++) {
|
|
92
|
+
bytes[i] = binaryString.charCodeAt(i);
|
|
93
|
+
}
|
|
94
|
+
const blob = new Blob([bytes], { type: "image/jpeg" });
|
|
95
|
+
return new File([blob], filename, { type: "image/jpeg" });
|
|
96
|
+
} catch (serverError) {
|
|
97
|
+
console.warn("Server-side HEIC conversion failed:", serverError);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const primaryMsg = primaryError instanceof Error ? primaryError.message : String(primaryError);
|
|
101
|
+
throw new Error(
|
|
102
|
+
`Unable to convert HEIC image (${primaryMsg}). Please export as JPEG from your Photos app.`
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
function generateId() {
|
|
108
|
+
const array = new Uint8Array(8);
|
|
109
|
+
crypto.getRandomValues(array);
|
|
110
|
+
const randomPart = Array.from(array, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
111
|
+
return `${Date.now()}-${randomPart}`;
|
|
112
|
+
}
|
|
113
|
+
function validateFileSize(file, maxSize = DEFAULT_MAX_FILE_SIZE) {
|
|
114
|
+
if (file.size > maxSize) {
|
|
115
|
+
return {
|
|
116
|
+
valid: false,
|
|
117
|
+
error: `File too large. Maximum size is ${Math.round(maxSize / 1024 / 1024)}MB, got ${(file.size / 1024 / 1024).toFixed(1)}MB`
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
return { valid: true };
|
|
121
|
+
}
|
|
122
|
+
async function getImageDimensions(file) {
|
|
123
|
+
return new Promise((resolve) => {
|
|
124
|
+
const img = new Image();
|
|
125
|
+
const url = URL.createObjectURL(file);
|
|
126
|
+
img.onload = () => {
|
|
127
|
+
URL.revokeObjectURL(url);
|
|
128
|
+
resolve({ width: img.naturalWidth, height: img.naturalHeight });
|
|
129
|
+
};
|
|
130
|
+
img.onerror = () => {
|
|
131
|
+
URL.revokeObjectURL(url);
|
|
132
|
+
resolve(null);
|
|
133
|
+
};
|
|
134
|
+
img.src = url;
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
function formatFileSize(bytes) {
|
|
138
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
139
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
140
|
+
return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
|
|
141
|
+
}
|
|
142
|
+
function getFileExtension(filename) {
|
|
143
|
+
const match = filename.match(/\.([^.]+)$/);
|
|
144
|
+
return match ? match[1].toLowerCase() : "";
|
|
145
|
+
}
|
|
146
|
+
function getMimeType(file) {
|
|
147
|
+
if (file.type) return file.type;
|
|
148
|
+
const ext = getFileExtension(file.name);
|
|
149
|
+
const mimeTypes = {
|
|
150
|
+
jpg: "image/jpeg",
|
|
151
|
+
jpeg: "image/jpeg",
|
|
152
|
+
png: "image/png",
|
|
153
|
+
gif: "image/gif",
|
|
154
|
+
webp: "image/webp",
|
|
155
|
+
avif: "image/avif",
|
|
156
|
+
bmp: "image/bmp",
|
|
157
|
+
tif: "image/tiff",
|
|
158
|
+
tiff: "image/tiff",
|
|
159
|
+
heic: "image/heic",
|
|
160
|
+
heif: "image/heif"
|
|
161
|
+
};
|
|
162
|
+
return mimeTypes[ext] || "application/octet-stream";
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// src/utils/csv-parser.ts
|
|
166
|
+
var DELIMITERS = [",", " ", ";", "|"];
|
|
167
|
+
function detectDelimiter(csvContent) {
|
|
168
|
+
const lines = csvContent.trim().split(/\r?\n/).slice(0, 5);
|
|
169
|
+
if (lines.length === 0) return ",";
|
|
170
|
+
const delimiterCounts = {
|
|
171
|
+
",": [],
|
|
172
|
+
" ": [],
|
|
173
|
+
";": [],
|
|
174
|
+
"|": []
|
|
175
|
+
};
|
|
176
|
+
for (const line of lines) {
|
|
177
|
+
let inQuotes = false;
|
|
178
|
+
const counts = { ",": 0, " ": 0, ";": 0, "|": 0 };
|
|
179
|
+
for (const char of line) {
|
|
180
|
+
if (char === '"') {
|
|
181
|
+
inQuotes = !inQuotes;
|
|
182
|
+
} else if (!inQuotes) {
|
|
183
|
+
if (char in counts) {
|
|
184
|
+
counts[char]++;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
for (const delim of DELIMITERS) {
|
|
189
|
+
delimiterCounts[delim].push(counts[delim]);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
let bestDelimiter = ",";
|
|
193
|
+
let bestScore = -1;
|
|
194
|
+
for (const delim of DELIMITERS) {
|
|
195
|
+
const counts = delimiterCounts[delim];
|
|
196
|
+
if (counts.length === 0) continue;
|
|
197
|
+
const nonZero = counts.filter((c) => c > 0);
|
|
198
|
+
if (nonZero.length === 0) continue;
|
|
199
|
+
const allSame = nonZero.every((c) => c === nonZero[0]);
|
|
200
|
+
const avgCount = nonZero.reduce((a, b) => a + b, 0) / nonZero.length;
|
|
201
|
+
const coverage = nonZero.length / counts.length;
|
|
202
|
+
const score = (allSame ? 100 : 0) + avgCount * coverage;
|
|
203
|
+
if (score > bestScore && avgCount > 0) {
|
|
204
|
+
bestScore = score;
|
|
205
|
+
bestDelimiter = delim;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return bestDelimiter;
|
|
209
|
+
}
|
|
210
|
+
function parseCsvRow(line, delimiter = ",") {
|
|
211
|
+
const result = [];
|
|
212
|
+
let current = "";
|
|
213
|
+
let inQuotes = false;
|
|
214
|
+
for (let i = 0; i < line.length; i++) {
|
|
215
|
+
const char = line[i];
|
|
216
|
+
if (char === '"') {
|
|
217
|
+
inQuotes = !inQuotes;
|
|
218
|
+
} else if (char === delimiter && !inQuotes) {
|
|
219
|
+
result.push(current.trim().replace(/^"|"$/g, ""));
|
|
220
|
+
current = "";
|
|
221
|
+
} else {
|
|
222
|
+
current += char;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
result.push(current.trim().replace(/^"|"$/g, ""));
|
|
226
|
+
return result;
|
|
227
|
+
}
|
|
228
|
+
function splitMultipleUrls(cellValue) {
|
|
229
|
+
if (!cellValue) return [];
|
|
230
|
+
return cellValue.split(",").map((url) => url.trim()).filter((url) => url.length > 0 && url.startsWith("http"));
|
|
231
|
+
}
|
|
232
|
+
function extractPathFromUrl(url) {
|
|
233
|
+
try {
|
|
234
|
+
return new URL(url).pathname;
|
|
235
|
+
} catch {
|
|
236
|
+
return url;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
function cellToString(cell) {
|
|
240
|
+
if (cell == null) return "";
|
|
241
|
+
if (typeof cell === "object" && "text" in cell) return cell.text;
|
|
242
|
+
if (typeof cell === "object" && "result" in cell) return String(cell.result ?? "");
|
|
243
|
+
return String(cell);
|
|
244
|
+
}
|
|
245
|
+
function getCsvHeaders(csvContent, delimiter) {
|
|
246
|
+
const lines = csvContent.trim().split(/\r?\n/);
|
|
247
|
+
if (lines.length === 0) return [];
|
|
248
|
+
const delim = delimiter ?? detectDelimiter(csvContent);
|
|
249
|
+
return parseCsvRow(lines[0], delim);
|
|
250
|
+
}
|
|
251
|
+
function findColumnIndex(headers, column) {
|
|
252
|
+
const headersLower = headers.map((h) => h.toLowerCase());
|
|
253
|
+
if (column) {
|
|
254
|
+
let columnIndex = headersLower.indexOf(column.toLowerCase());
|
|
255
|
+
if (columnIndex !== -1) return columnIndex;
|
|
256
|
+
columnIndex = headers.indexOf(column);
|
|
257
|
+
if (columnIndex !== -1) return columnIndex;
|
|
258
|
+
}
|
|
259
|
+
return headersLower.findIndex((h) => h === "url");
|
|
260
|
+
}
|
|
261
|
+
var defaultUrlValidator = (url) => {
|
|
262
|
+
try {
|
|
263
|
+
const parsed = new URL(url);
|
|
264
|
+
if (!["http:", "https:"].includes(parsed.protocol)) {
|
|
265
|
+
return { valid: false, error: "URL must use http or https protocol" };
|
|
266
|
+
}
|
|
267
|
+
return { valid: true };
|
|
268
|
+
} catch {
|
|
269
|
+
return { valid: false, error: "Invalid URL format" };
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
var sirvUrlValidator = (url) => {
|
|
273
|
+
const sirvMatch = url.match(/^https?:\/\/([^.]+)\.sirv\.com(\/[^?#]+)/);
|
|
274
|
+
if (sirvMatch) {
|
|
275
|
+
return { valid: true };
|
|
276
|
+
}
|
|
277
|
+
const imageMatch = url.match(
|
|
278
|
+
/^https?:\/\/[^/]+(\/[^?#]+\.(jpe?g|png|gif|webp|avif|bmp|tiff?))$/i
|
|
279
|
+
);
|
|
280
|
+
if (imageMatch) {
|
|
281
|
+
return { valid: true };
|
|
282
|
+
}
|
|
283
|
+
return { valid: false, error: "Not a valid Sirv or image URL" };
|
|
284
|
+
};
|
|
285
|
+
async function parseExcelArrayBuffer(arrayBuffer) {
|
|
286
|
+
const ExcelJS = await import('exceljs');
|
|
287
|
+
const workbook = new ExcelJS.default.Workbook();
|
|
288
|
+
await workbook.xlsx.load(arrayBuffer);
|
|
289
|
+
const worksheet = workbook.worksheets[0];
|
|
290
|
+
if (!worksheet) return [];
|
|
291
|
+
const rows = [];
|
|
292
|
+
worksheet.eachRow((row) => {
|
|
293
|
+
const values = row.values;
|
|
294
|
+
rows.push(values.slice(1));
|
|
295
|
+
});
|
|
296
|
+
return rows;
|
|
297
|
+
}
|
|
298
|
+
function getExcelHeaders(rows) {
|
|
299
|
+
if (rows.length === 0) return [];
|
|
300
|
+
return rows[0].map((cell, i) => cellToString(cell) || `Column ${i + 1}`);
|
|
301
|
+
}
|
|
302
|
+
function getExcelSampleRows(rows, urlColumnIndex) {
|
|
303
|
+
if (rows.length < 2) return [];
|
|
304
|
+
const sampleRows = [];
|
|
305
|
+
for (let i = 1; i < rows.length && sampleRows.length < 3; i++) {
|
|
306
|
+
const urlCell = cellToString(rows[i][urlColumnIndex]).trim();
|
|
307
|
+
if (urlCell && urlCell.length > 0) {
|
|
308
|
+
sampleRows.push(rows[i].map((cell) => cellToString(cell)));
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
if (sampleRows.length === 0) {
|
|
312
|
+
return rows.slice(1, 4).map((row) => row.map((cell) => cellToString(cell)));
|
|
313
|
+
}
|
|
314
|
+
return sampleRows;
|
|
315
|
+
}
|
|
316
|
+
function getCsvSampleRows(lines, urlColumnIndex, delimiter = ",") {
|
|
317
|
+
if (lines.length < 2) return [];
|
|
318
|
+
const sampleRows = [];
|
|
319
|
+
for (let i = 1; i < lines.length && sampleRows.length < 3; i++) {
|
|
320
|
+
const row = parseCsvRow(lines[i], delimiter);
|
|
321
|
+
const urlCell = row[urlColumnIndex]?.trim();
|
|
322
|
+
if (urlCell && urlCell.length > 0) {
|
|
323
|
+
sampleRows.push(row);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
if (sampleRows.length === 0) {
|
|
327
|
+
for (let i = 1; i <= Math.min(3, lines.length - 1); i++) {
|
|
328
|
+
sampleRows.push(parseCsvRow(lines[i], delimiter));
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
return sampleRows;
|
|
332
|
+
}
|
|
333
|
+
function estimateExcelImageCount(rows, columnIndex) {
|
|
334
|
+
if (rows.length < 2 || columnIndex < 0) return rows.length - 1;
|
|
335
|
+
let count = 0;
|
|
336
|
+
for (let i = 1; i < rows.length; i++) {
|
|
337
|
+
const cellValue = cellToString(rows[i][columnIndex]).trim();
|
|
338
|
+
if (cellValue) {
|
|
339
|
+
count += splitMultipleUrls(cellValue).length;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
return count || rows.length - 1;
|
|
343
|
+
}
|
|
344
|
+
function estimateCsvImageCount(lines, columnIndex, delimiter = ",") {
|
|
345
|
+
if (lines.length < 2 || columnIndex < 0) return lines.length - 1;
|
|
346
|
+
let count = 0;
|
|
347
|
+
for (let i = 1; i < lines.length; i++) {
|
|
348
|
+
const values = parseCsvRow(lines[i], delimiter);
|
|
349
|
+
const cellValue = values[columnIndex]?.trim();
|
|
350
|
+
if (cellValue) {
|
|
351
|
+
count += splitMultipleUrls(cellValue).length;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
return count || lines.length - 1;
|
|
355
|
+
}
|
|
356
|
+
function parseCsvClient(csvContent, options = {}) {
|
|
357
|
+
const { validator = defaultUrlValidator } = options;
|
|
358
|
+
const lines = csvContent.trim().split(/\r?\n/);
|
|
359
|
+
const delimiter = detectDelimiter(csvContent);
|
|
360
|
+
const headers = getCsvHeaders(csvContent, delimiter);
|
|
361
|
+
const rowCount = lines.length - 1;
|
|
362
|
+
const headersLower = headers.map((h) => h.toLowerCase());
|
|
363
|
+
let urlColumnIndex = headersLower.findIndex(
|
|
364
|
+
(h) => h === "url" || h === "image" || h === "images" || h === "image_url"
|
|
365
|
+
);
|
|
366
|
+
if (urlColumnIndex === -1) {
|
|
367
|
+
for (let col = 0; col < headers.length && urlColumnIndex === -1; col++) {
|
|
368
|
+
for (let row = 1; row < Math.min(100, lines.length); row++) {
|
|
369
|
+
const values = parseCsvRow(lines[row], delimiter);
|
|
370
|
+
const cell = values[col]?.trim();
|
|
371
|
+
if (cell && cell.startsWith("http")) {
|
|
372
|
+
urlColumnIndex = col;
|
|
373
|
+
break;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
if (urlColumnIndex === -1) urlColumnIndex = 0;
|
|
379
|
+
const sampleRows = getCsvSampleRows(lines, urlColumnIndex, delimiter);
|
|
380
|
+
const estimatedImageCounts = headers.map(
|
|
381
|
+
(_, colIndex) => estimateCsvImageCount(lines, colIndex, delimiter)
|
|
382
|
+
);
|
|
383
|
+
if (options.previewOnly) {
|
|
384
|
+
return {
|
|
385
|
+
headers,
|
|
386
|
+
sampleRows,
|
|
387
|
+
rowCount,
|
|
388
|
+
estimatedImageCounts,
|
|
389
|
+
urls: [],
|
|
390
|
+
validCount: 0,
|
|
391
|
+
invalidCount: 0,
|
|
392
|
+
totalCount: 0
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
const columnIndex = findColumnIndex(headers, options.column);
|
|
396
|
+
if (columnIndex === -1) {
|
|
397
|
+
throw new Error("Column not found in CSV");
|
|
398
|
+
}
|
|
399
|
+
const urls = [];
|
|
400
|
+
for (let i = 1; i < lines.length; i++) {
|
|
401
|
+
const values = parseCsvRow(lines[i], delimiter);
|
|
402
|
+
const cellValue = values[columnIndex]?.trim();
|
|
403
|
+
if (cellValue) {
|
|
404
|
+
for (const url of splitMultipleUrls(cellValue)) {
|
|
405
|
+
const validation = validator(url);
|
|
406
|
+
urls.push({
|
|
407
|
+
url,
|
|
408
|
+
path: extractPathFromUrl(url),
|
|
409
|
+
valid: validation.valid,
|
|
410
|
+
error: validation.error
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
const validCount = urls.filter((u) => u.valid).length;
|
|
416
|
+
return {
|
|
417
|
+
headers,
|
|
418
|
+
sampleRows,
|
|
419
|
+
rowCount,
|
|
420
|
+
estimatedImageCounts,
|
|
421
|
+
urls,
|
|
422
|
+
validCount,
|
|
423
|
+
invalidCount: urls.length - validCount,
|
|
424
|
+
totalCount: urls.length
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
async function parseExcelClient(arrayBuffer, options = {}) {
|
|
428
|
+
const { validator = defaultUrlValidator } = options;
|
|
429
|
+
const rows = await parseExcelArrayBuffer(arrayBuffer);
|
|
430
|
+
const headers = getExcelHeaders(rows);
|
|
431
|
+
const rowCount = rows.length - 1;
|
|
432
|
+
const headersLower = headers.map((h) => h.toLowerCase());
|
|
433
|
+
let urlColumnIndex = headersLower.findIndex(
|
|
434
|
+
(h) => h === "url" || h === "image" || h === "images" || h === "image_url"
|
|
435
|
+
);
|
|
436
|
+
if (urlColumnIndex === -1) {
|
|
437
|
+
for (let col = 0; col < headers.length && urlColumnIndex === -1; col++) {
|
|
438
|
+
for (let row = 1; row < Math.min(100, rows.length); row++) {
|
|
439
|
+
const cell = cellToString(rows[row][col]).trim();
|
|
440
|
+
if (cell && cell.startsWith("http")) {
|
|
441
|
+
urlColumnIndex = col;
|
|
442
|
+
break;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
if (urlColumnIndex === -1) urlColumnIndex = 0;
|
|
448
|
+
const sampleRows = getExcelSampleRows(rows, urlColumnIndex);
|
|
449
|
+
const estimatedImageCounts = headers.map(
|
|
450
|
+
(_, colIndex) => estimateExcelImageCount(rows, colIndex)
|
|
451
|
+
);
|
|
452
|
+
if (options.previewOnly) {
|
|
453
|
+
return {
|
|
454
|
+
headers,
|
|
455
|
+
sampleRows,
|
|
456
|
+
rowCount,
|
|
457
|
+
estimatedImageCounts,
|
|
458
|
+
urls: [],
|
|
459
|
+
validCount: 0,
|
|
460
|
+
invalidCount: 0,
|
|
461
|
+
totalCount: 0
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
const columnIndex = findColumnIndex(headers, options.column);
|
|
465
|
+
if (columnIndex === -1) {
|
|
466
|
+
throw new Error("Column not found in Excel file");
|
|
467
|
+
}
|
|
468
|
+
const urls = [];
|
|
469
|
+
for (let i = 1; i < rows.length; i++) {
|
|
470
|
+
const cellValue = cellToString(rows[i][columnIndex]).trim();
|
|
471
|
+
if (cellValue) {
|
|
472
|
+
for (const url of splitMultipleUrls(cellValue)) {
|
|
473
|
+
const validation = validator(url);
|
|
474
|
+
urls.push({
|
|
475
|
+
url,
|
|
476
|
+
path: extractPathFromUrl(url),
|
|
477
|
+
valid: validation.valid,
|
|
478
|
+
error: validation.error
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
const validCount = urls.filter((u) => u.valid).length;
|
|
484
|
+
return {
|
|
485
|
+
headers,
|
|
486
|
+
sampleRows,
|
|
487
|
+
rowCount,
|
|
488
|
+
estimatedImageCounts,
|
|
489
|
+
urls,
|
|
490
|
+
validCount,
|
|
491
|
+
invalidCount: urls.length - validCount,
|
|
492
|
+
totalCount: urls.length
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
function isSpreadsheetFile(file) {
|
|
496
|
+
const ext = file.name.toLowerCase();
|
|
497
|
+
return ext.endsWith(".csv") || ext.endsWith(".xlsx") || ext.endsWith(".xls") || ext.endsWith(".txt") || file.type === "text/csv" || file.type === "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" || file.type === "application/vnd.ms-excel";
|
|
498
|
+
}
|
|
499
|
+
function DropZone({
|
|
500
|
+
onFiles,
|
|
501
|
+
onSpreadsheet,
|
|
502
|
+
accept = ["image/*"],
|
|
503
|
+
maxFiles = 50,
|
|
504
|
+
maxFileSize = 10 * 1024 * 1024,
|
|
505
|
+
disabled = false,
|
|
506
|
+
compact = false,
|
|
507
|
+
className,
|
|
508
|
+
labels = {},
|
|
509
|
+
children
|
|
510
|
+
}) {
|
|
511
|
+
const [isDragOver, setIsDragOver] = react.useState(false);
|
|
512
|
+
const [isConverting, setIsConverting] = react.useState(false);
|
|
513
|
+
const [convertingCount, setConvertingCount] = react.useState(0);
|
|
514
|
+
const inputRef = react.useRef(null);
|
|
515
|
+
const processFiles = react.useCallback(
|
|
516
|
+
async (fileList) => {
|
|
517
|
+
const files = Array.from(fileList).slice(0, maxFiles);
|
|
518
|
+
const spreadsheetFile = files.find(isSpreadsheetFile);
|
|
519
|
+
if (spreadsheetFile && onSpreadsheet) {
|
|
520
|
+
onSpreadsheet(spreadsheetFile);
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
const imageFiles = files.filter(isImageFile);
|
|
524
|
+
if (imageFiles.length === 0) return;
|
|
525
|
+
const heifFiles = imageFiles.filter(isHeifFile);
|
|
526
|
+
const regularFiles = imageFiles.filter((f) => !isHeifFile(f));
|
|
527
|
+
setIsConverting(heifFiles.length > 0);
|
|
528
|
+
setConvertingCount(heifFiles.length);
|
|
529
|
+
const processedFiles = [];
|
|
530
|
+
for (const file of regularFiles) {
|
|
531
|
+
const sizeValidation = validateFileSize(file, maxFileSize);
|
|
532
|
+
if (!sizeValidation.valid) {
|
|
533
|
+
processedFiles.push({
|
|
534
|
+
id: generateId(),
|
|
535
|
+
file,
|
|
536
|
+
filename: file.name,
|
|
537
|
+
previewUrl: "",
|
|
538
|
+
status: "error",
|
|
539
|
+
progress: 0,
|
|
540
|
+
error: sizeValidation.error
|
|
541
|
+
});
|
|
542
|
+
continue;
|
|
543
|
+
}
|
|
544
|
+
const dimensions = await getImageDimensions(file);
|
|
545
|
+
processedFiles.push({
|
|
546
|
+
id: generateId(),
|
|
547
|
+
file,
|
|
548
|
+
filename: file.name,
|
|
549
|
+
previewUrl: URL.createObjectURL(file),
|
|
550
|
+
dimensions: dimensions || void 0,
|
|
551
|
+
size: file.size,
|
|
552
|
+
status: "pending",
|
|
553
|
+
progress: 0
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
for (const file of heifFiles) {
|
|
557
|
+
try {
|
|
558
|
+
const converted = await convertHeicWithFallback(file);
|
|
559
|
+
const sizeValidation = validateFileSize(converted, maxFileSize);
|
|
560
|
+
if (!sizeValidation.valid) {
|
|
561
|
+
processedFiles.push({
|
|
562
|
+
id: generateId(),
|
|
563
|
+
file: converted,
|
|
564
|
+
filename: converted.name,
|
|
565
|
+
previewUrl: "",
|
|
566
|
+
status: "error",
|
|
567
|
+
progress: 0,
|
|
568
|
+
error: sizeValidation.error
|
|
569
|
+
});
|
|
570
|
+
continue;
|
|
571
|
+
}
|
|
572
|
+
const dimensions = await getImageDimensions(converted);
|
|
573
|
+
processedFiles.push({
|
|
574
|
+
id: generateId(),
|
|
575
|
+
file: converted,
|
|
576
|
+
filename: converted.name,
|
|
577
|
+
previewUrl: URL.createObjectURL(converted),
|
|
578
|
+
dimensions: dimensions || void 0,
|
|
579
|
+
size: converted.size,
|
|
580
|
+
status: "pending",
|
|
581
|
+
progress: 0
|
|
582
|
+
});
|
|
583
|
+
} catch (err) {
|
|
584
|
+
processedFiles.push({
|
|
585
|
+
id: generateId(),
|
|
586
|
+
file,
|
|
587
|
+
filename: file.name,
|
|
588
|
+
previewUrl: "",
|
|
589
|
+
status: "error",
|
|
590
|
+
progress: 0,
|
|
591
|
+
error: err instanceof Error ? err.message : "Failed to convert HEIC file"
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
setConvertingCount((c) => c - 1);
|
|
595
|
+
}
|
|
596
|
+
setIsConverting(false);
|
|
597
|
+
setConvertingCount(0);
|
|
598
|
+
if (processedFiles.length > 0) {
|
|
599
|
+
onFiles(processedFiles);
|
|
600
|
+
}
|
|
601
|
+
},
|
|
602
|
+
[maxFiles, maxFileSize, onFiles, onSpreadsheet]
|
|
603
|
+
);
|
|
604
|
+
const handleDragOver = react.useCallback(
|
|
605
|
+
(e) => {
|
|
606
|
+
e.preventDefault();
|
|
607
|
+
e.stopPropagation();
|
|
608
|
+
if (!disabled) {
|
|
609
|
+
setIsDragOver(true);
|
|
610
|
+
}
|
|
611
|
+
},
|
|
612
|
+
[disabled]
|
|
613
|
+
);
|
|
614
|
+
const handleDragLeave = react.useCallback((e) => {
|
|
615
|
+
e.preventDefault();
|
|
616
|
+
e.stopPropagation();
|
|
617
|
+
setIsDragOver(false);
|
|
618
|
+
}, []);
|
|
619
|
+
const handleDrop = react.useCallback(
|
|
620
|
+
async (e) => {
|
|
621
|
+
e.preventDefault();
|
|
622
|
+
e.stopPropagation();
|
|
623
|
+
setIsDragOver(false);
|
|
624
|
+
if (disabled) return;
|
|
625
|
+
const { files } = e.dataTransfer;
|
|
626
|
+
if (files.length > 0) {
|
|
627
|
+
await processFiles(files);
|
|
628
|
+
}
|
|
629
|
+
},
|
|
630
|
+
[disabled, processFiles]
|
|
631
|
+
);
|
|
632
|
+
const handleChange = react.useCallback(
|
|
633
|
+
async (e) => {
|
|
634
|
+
const { files } = e.target;
|
|
635
|
+
if (files && files.length > 0) {
|
|
636
|
+
await processFiles(files);
|
|
637
|
+
}
|
|
638
|
+
e.target.value = "";
|
|
639
|
+
},
|
|
640
|
+
[processFiles]
|
|
641
|
+
);
|
|
642
|
+
const handleClick = react.useCallback(() => {
|
|
643
|
+
if (!disabled) {
|
|
644
|
+
inputRef.current?.click();
|
|
645
|
+
}
|
|
646
|
+
}, [disabled]);
|
|
647
|
+
const handleKeyDown = react.useCallback(
|
|
648
|
+
(e) => {
|
|
649
|
+
if ((e.key === "Enter" || e.key === " ") && !disabled) {
|
|
650
|
+
e.preventDefault();
|
|
651
|
+
inputRef.current?.click();
|
|
652
|
+
}
|
|
653
|
+
},
|
|
654
|
+
[disabled]
|
|
655
|
+
);
|
|
656
|
+
const acceptString = accept.join(",") || ACCEPTED_IMAGE_FORMATS;
|
|
657
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
658
|
+
"div",
|
|
659
|
+
{
|
|
660
|
+
className: clsx2__default.default(
|
|
661
|
+
"sirv-dropzone",
|
|
662
|
+
isDragOver && "sirv-dropzone--drag-over",
|
|
663
|
+
disabled && "sirv-dropzone--disabled",
|
|
664
|
+
compact && "sirv-dropzone--compact",
|
|
665
|
+
isConverting && "sirv-dropzone--converting",
|
|
666
|
+
className
|
|
667
|
+
),
|
|
668
|
+
onDragOver: handleDragOver,
|
|
669
|
+
onDragLeave: handleDragLeave,
|
|
670
|
+
onDrop: handleDrop,
|
|
671
|
+
onClick: handleClick,
|
|
672
|
+
onKeyDown: handleKeyDown,
|
|
673
|
+
role: "button",
|
|
674
|
+
tabIndex: disabled ? -1 : 0,
|
|
675
|
+
"aria-disabled": disabled,
|
|
676
|
+
"aria-label": labels.dropzone || "Drop files here or click to browse",
|
|
677
|
+
children: [
|
|
678
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
679
|
+
"input",
|
|
680
|
+
{
|
|
681
|
+
ref: inputRef,
|
|
682
|
+
type: "file",
|
|
683
|
+
accept: acceptString,
|
|
684
|
+
multiple: maxFiles > 1,
|
|
685
|
+
onChange: handleChange,
|
|
686
|
+
disabled,
|
|
687
|
+
className: "sirv-dropzone__input",
|
|
688
|
+
"aria-hidden": "true"
|
|
689
|
+
}
|
|
690
|
+
),
|
|
691
|
+
children || /* @__PURE__ */ jsxRuntime.jsx("div", { className: "sirv-dropzone__content", children: isConverting ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
692
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "sirv-dropzone__spinner" }),
|
|
693
|
+
/* @__PURE__ */ jsxRuntime.jsxs("p", { className: "sirv-dropzone__text", children: [
|
|
694
|
+
"Converting ",
|
|
695
|
+
convertingCount,
|
|
696
|
+
" HEIC file",
|
|
697
|
+
convertingCount !== 1 ? "s" : "",
|
|
698
|
+
"..."
|
|
699
|
+
] })
|
|
700
|
+
] }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
701
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
702
|
+
"svg",
|
|
703
|
+
{
|
|
704
|
+
className: "sirv-dropzone__icon",
|
|
705
|
+
viewBox: "0 0 24 24",
|
|
706
|
+
fill: "none",
|
|
707
|
+
stroke: "currentColor",
|
|
708
|
+
strokeWidth: "2",
|
|
709
|
+
strokeLinecap: "round",
|
|
710
|
+
strokeLinejoin: "round",
|
|
711
|
+
children: [
|
|
712
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" }),
|
|
713
|
+
/* @__PURE__ */ jsxRuntime.jsx("polyline", { points: "17 8 12 3 7 8" }),
|
|
714
|
+
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "12", y1: "3", x2: "12", y2: "15" })
|
|
715
|
+
]
|
|
716
|
+
}
|
|
717
|
+
),
|
|
718
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "sirv-dropzone__text", children: labels.dropzone || "Drop files here or click to browse" }),
|
|
719
|
+
!compact && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "sirv-dropzone__hint", children: labels.dropzoneHint || "Supports JPG, PNG, WebP, GIF, HEIC up to 10MB" })
|
|
720
|
+
] }) })
|
|
721
|
+
]
|
|
722
|
+
}
|
|
723
|
+
);
|
|
724
|
+
}
|
|
725
|
+
function FileList({
|
|
726
|
+
files,
|
|
727
|
+
onRemove,
|
|
728
|
+
onRetry,
|
|
729
|
+
showThumbnails = true,
|
|
730
|
+
className,
|
|
731
|
+
labels = {}
|
|
732
|
+
}) {
|
|
733
|
+
if (files.length === 0) return null;
|
|
734
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: clsx2__default.default("sirv-filelist", className), children: files.map((file) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
735
|
+
FileItem,
|
|
736
|
+
{
|
|
737
|
+
file,
|
|
738
|
+
onRemove,
|
|
739
|
+
onRetry,
|
|
740
|
+
showThumbnail: showThumbnails,
|
|
741
|
+
labels
|
|
742
|
+
},
|
|
743
|
+
file.id
|
|
744
|
+
)) });
|
|
745
|
+
}
|
|
746
|
+
function FileItem({ file, onRemove, onRetry, showThumbnail, labels = {} }) {
|
|
747
|
+
const statusText = {
|
|
748
|
+
pending: "",
|
|
749
|
+
uploading: labels.uploading || "Uploading...",
|
|
750
|
+
processing: labels.processing || "Processing...",
|
|
751
|
+
success: labels.success || "Uploaded",
|
|
752
|
+
error: labels.error || "Failed",
|
|
753
|
+
conflict: "Conflict"
|
|
754
|
+
};
|
|
755
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
756
|
+
"div",
|
|
757
|
+
{
|
|
758
|
+
className: clsx2__default.default(
|
|
759
|
+
"sirv-filelist__item",
|
|
760
|
+
`sirv-filelist__item--${file.status}`,
|
|
761
|
+
file.error && "sirv-filelist__item--has-error"
|
|
762
|
+
),
|
|
763
|
+
children: [
|
|
764
|
+
showThumbnail && file.previewUrl && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "sirv-filelist__thumbnail", children: /* @__PURE__ */ jsxRuntime.jsx("img", { src: file.previewUrl, alt: "" }) }),
|
|
765
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "sirv-filelist__info", children: [
|
|
766
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "sirv-filelist__name", title: file.filename, children: file.filename }),
|
|
767
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "sirv-filelist__meta", children: [
|
|
768
|
+
file.size && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "sirv-filelist__size", children: formatFileSize(file.size) }),
|
|
769
|
+
file.dimensions && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "sirv-filelist__dimensions", children: [
|
|
770
|
+
file.dimensions.width,
|
|
771
|
+
" \xD7 ",
|
|
772
|
+
file.dimensions.height
|
|
773
|
+
] }),
|
|
774
|
+
file.status !== "pending" && /* @__PURE__ */ jsxRuntime.jsx("span", { className: `sirv-filelist__status sirv-filelist__status--${file.status}`, children: statusText[file.status] })
|
|
775
|
+
] }),
|
|
776
|
+
file.error && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "sirv-filelist__error", children: file.error })
|
|
777
|
+
] }),
|
|
778
|
+
(file.status === "uploading" || file.status === "processing") && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "sirv-filelist__progress", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
779
|
+
"div",
|
|
780
|
+
{
|
|
781
|
+
className: "sirv-filelist__progress-bar",
|
|
782
|
+
style: { width: `${file.progress}%` }
|
|
783
|
+
}
|
|
784
|
+
) }),
|
|
785
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "sirv-filelist__actions", children: [
|
|
786
|
+
file.status === "error" && onRetry && /* @__PURE__ */ jsxRuntime.jsx(
|
|
787
|
+
"button",
|
|
788
|
+
{
|
|
789
|
+
type: "button",
|
|
790
|
+
className: "sirv-filelist__action sirv-filelist__action--retry",
|
|
791
|
+
onClick: () => onRetry(file.id),
|
|
792
|
+
"aria-label": labels.retry || "Retry upload",
|
|
793
|
+
title: labels.retry || "Retry",
|
|
794
|
+
children: /* @__PURE__ */ jsxRuntime.jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [
|
|
795
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M1 4v6h6" }),
|
|
796
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M3.51 15a9 9 0 1 0 2.13-9.36L1 10" })
|
|
797
|
+
] })
|
|
798
|
+
}
|
|
799
|
+
),
|
|
800
|
+
onRemove && file.status !== "uploading" && /* @__PURE__ */ jsxRuntime.jsx(
|
|
801
|
+
"button",
|
|
802
|
+
{
|
|
803
|
+
type: "button",
|
|
804
|
+
className: "sirv-filelist__action sirv-filelist__action--remove",
|
|
805
|
+
onClick: () => onRemove(file.id),
|
|
806
|
+
"aria-label": labels.remove || "Remove file",
|
|
807
|
+
title: labels.remove || "Remove",
|
|
808
|
+
children: /* @__PURE__ */ jsxRuntime.jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [
|
|
809
|
+
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" }),
|
|
810
|
+
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" })
|
|
811
|
+
] })
|
|
812
|
+
}
|
|
813
|
+
),
|
|
814
|
+
file.status === "success" && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "sirv-filelist__check", children: /* @__PURE__ */ jsxRuntime.jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: /* @__PURE__ */ jsxRuntime.jsx("polyline", { points: "20 6 9 17 4 12" }) }) })
|
|
815
|
+
] })
|
|
816
|
+
]
|
|
817
|
+
}
|
|
818
|
+
);
|
|
819
|
+
}
|
|
820
|
+
function FileListSummary({ files, className }) {
|
|
821
|
+
const pending = files.filter((f) => f.status === "pending").length;
|
|
822
|
+
const uploading = files.filter((f) => f.status === "uploading" || f.status === "processing").length;
|
|
823
|
+
const success = files.filter((f) => f.status === "success").length;
|
|
824
|
+
const error = files.filter((f) => f.status === "error").length;
|
|
825
|
+
if (files.length === 0) return null;
|
|
826
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: clsx2__default.default("sirv-filelist-summary", className), children: [
|
|
827
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "sirv-filelist-summary__total", children: [
|
|
828
|
+
files.length,
|
|
829
|
+
" files"
|
|
830
|
+
] }),
|
|
831
|
+
pending > 0 && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "sirv-filelist-summary__pending", children: [
|
|
832
|
+
pending,
|
|
833
|
+
" pending"
|
|
834
|
+
] }),
|
|
835
|
+
uploading > 0 && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "sirv-filelist-summary__uploading", children: [
|
|
836
|
+
uploading,
|
|
837
|
+
" uploading"
|
|
838
|
+
] }),
|
|
839
|
+
success > 0 && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "sirv-filelist-summary__success", children: [
|
|
840
|
+
success,
|
|
841
|
+
" uploaded"
|
|
842
|
+
] }),
|
|
843
|
+
error > 0 && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "sirv-filelist-summary__error", children: [
|
|
844
|
+
error,
|
|
845
|
+
" failed"
|
|
846
|
+
] })
|
|
847
|
+
] });
|
|
848
|
+
}
|
|
849
|
+
function FilePicker({
|
|
850
|
+
endpoint,
|
|
851
|
+
isOpen,
|
|
852
|
+
onClose,
|
|
853
|
+
onSelect,
|
|
854
|
+
fileType = "image",
|
|
855
|
+
multiple = false,
|
|
856
|
+
initialPath = "/",
|
|
857
|
+
className,
|
|
858
|
+
labels = {}
|
|
859
|
+
}) {
|
|
860
|
+
const [currentPath, setCurrentPath] = react.useState(initialPath);
|
|
861
|
+
const [items, setItems] = react.useState([]);
|
|
862
|
+
const [selectedItems, setSelectedItems] = react.useState([]);
|
|
863
|
+
const [isLoading, setIsLoading] = react.useState(false);
|
|
864
|
+
const [error, setError] = react.useState(null);
|
|
865
|
+
const [searchQuery, setSearchQuery] = react.useState("");
|
|
866
|
+
const searchTimeoutRef = react.useRef(null);
|
|
867
|
+
const fetchItems = react.useCallback(
|
|
868
|
+
async (path, search) => {
|
|
869
|
+
setIsLoading(true);
|
|
870
|
+
setError(null);
|
|
871
|
+
try {
|
|
872
|
+
const params = new URLSearchParams({ path });
|
|
873
|
+
if (fileType !== "all") params.set("type", fileType);
|
|
874
|
+
if (search) params.set("search", search);
|
|
875
|
+
const response = await fetch(`${endpoint}/browse?${params}`);
|
|
876
|
+
if (!response.ok) {
|
|
877
|
+
throw new Error(`Failed to load files: ${response.status}`);
|
|
878
|
+
}
|
|
879
|
+
const data = await response.json();
|
|
880
|
+
if (!data.success) {
|
|
881
|
+
throw new Error(data.error || "Failed to load files");
|
|
882
|
+
}
|
|
883
|
+
setItems(data.items || []);
|
|
884
|
+
setCurrentPath(data.path);
|
|
885
|
+
} catch (err) {
|
|
886
|
+
setError(err instanceof Error ? err.message : "Failed to load files");
|
|
887
|
+
setItems([]);
|
|
888
|
+
} finally {
|
|
889
|
+
setIsLoading(false);
|
|
890
|
+
}
|
|
891
|
+
},
|
|
892
|
+
[endpoint, fileType]
|
|
893
|
+
);
|
|
894
|
+
react.useEffect(() => {
|
|
895
|
+
if (isOpen) {
|
|
896
|
+
setSelectedItems([]);
|
|
897
|
+
setSearchQuery("");
|
|
898
|
+
fetchItems(initialPath);
|
|
899
|
+
}
|
|
900
|
+
}, [isOpen, initialPath, fetchItems]);
|
|
901
|
+
react.useEffect(() => {
|
|
902
|
+
if (!isOpen) return;
|
|
903
|
+
if (searchTimeoutRef.current) {
|
|
904
|
+
clearTimeout(searchTimeoutRef.current);
|
|
905
|
+
}
|
|
906
|
+
searchTimeoutRef.current = setTimeout(() => {
|
|
907
|
+
fetchItems(currentPath, searchQuery || void 0);
|
|
908
|
+
}, 300);
|
|
909
|
+
return () => {
|
|
910
|
+
if (searchTimeoutRef.current) {
|
|
911
|
+
clearTimeout(searchTimeoutRef.current);
|
|
912
|
+
}
|
|
913
|
+
};
|
|
914
|
+
}, [searchQuery, currentPath, isOpen, fetchItems]);
|
|
915
|
+
const handleNavigate = react.useCallback((path) => {
|
|
916
|
+
setSearchQuery("");
|
|
917
|
+
setCurrentPath(path);
|
|
918
|
+
}, []);
|
|
919
|
+
const handleGoUp = react.useCallback(() => {
|
|
920
|
+
const parentPath = currentPath.split("/").slice(0, -1).join("/") || "/";
|
|
921
|
+
handleNavigate(parentPath);
|
|
922
|
+
}, [currentPath, handleNavigate]);
|
|
923
|
+
const handleItemClick = react.useCallback(
|
|
924
|
+
(item) => {
|
|
925
|
+
if (item.type === "folder") {
|
|
926
|
+
handleNavigate(item.path);
|
|
927
|
+
return;
|
|
928
|
+
}
|
|
929
|
+
if (multiple) {
|
|
930
|
+
setSelectedItems((prev) => {
|
|
931
|
+
const isSelected = prev.some((i) => i.path === item.path);
|
|
932
|
+
if (isSelected) {
|
|
933
|
+
return prev.filter((i) => i.path !== item.path);
|
|
934
|
+
}
|
|
935
|
+
return [...prev, item];
|
|
936
|
+
});
|
|
937
|
+
} else {
|
|
938
|
+
setSelectedItems([item]);
|
|
939
|
+
}
|
|
940
|
+
},
|
|
941
|
+
[multiple, handleNavigate]
|
|
942
|
+
);
|
|
943
|
+
const handleSelect = react.useCallback(() => {
|
|
944
|
+
if (selectedItems.length > 0) {
|
|
945
|
+
onSelect(selectedItems);
|
|
946
|
+
onClose();
|
|
947
|
+
}
|
|
948
|
+
}, [selectedItems, onSelect, onClose]);
|
|
949
|
+
const handleKeyDown = react.useCallback(
|
|
950
|
+
(e) => {
|
|
951
|
+
if (e.key === "Escape") {
|
|
952
|
+
onClose();
|
|
953
|
+
}
|
|
954
|
+
},
|
|
955
|
+
[onClose]
|
|
956
|
+
);
|
|
957
|
+
const breadcrumbs = currentPath.split("/").filter(Boolean);
|
|
958
|
+
if (!isOpen) return null;
|
|
959
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
960
|
+
"div",
|
|
961
|
+
{
|
|
962
|
+
className: clsx2__default.default("sirv-filepicker-overlay", className),
|
|
963
|
+
onClick: onClose,
|
|
964
|
+
onKeyDown: handleKeyDown,
|
|
965
|
+
role: "dialog",
|
|
966
|
+
"aria-modal": "true",
|
|
967
|
+
"aria-label": labels.title || "Select files from Sirv",
|
|
968
|
+
children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "sirv-filepicker", onClick: (e) => e.stopPropagation(), children: [
|
|
969
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "sirv-filepicker__header", children: [
|
|
970
|
+
/* @__PURE__ */ jsxRuntime.jsx("h2", { className: "sirv-filepicker__title", children: labels.title || "Select from Sirv" }),
|
|
971
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
972
|
+
"button",
|
|
973
|
+
{
|
|
974
|
+
type: "button",
|
|
975
|
+
className: "sirv-filepicker__close",
|
|
976
|
+
onClick: onClose,
|
|
977
|
+
"aria-label": "Close",
|
|
978
|
+
children: /* @__PURE__ */ jsxRuntime.jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [
|
|
979
|
+
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" }),
|
|
980
|
+
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" })
|
|
981
|
+
] })
|
|
982
|
+
}
|
|
983
|
+
)
|
|
984
|
+
] }),
|
|
985
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "sirv-filepicker__toolbar", children: [
|
|
986
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "sirv-filepicker__breadcrumbs", children: [
|
|
987
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
988
|
+
"button",
|
|
989
|
+
{
|
|
990
|
+
type: "button",
|
|
991
|
+
className: "sirv-filepicker__breadcrumb",
|
|
992
|
+
onClick: () => handleNavigate("/"),
|
|
993
|
+
children: /* @__PURE__ */ jsxRuntime.jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" }) })
|
|
994
|
+
}
|
|
995
|
+
),
|
|
996
|
+
breadcrumbs.map((part, index) => /* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
|
|
997
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "sirv-filepicker__breadcrumb-separator", children: "/" }),
|
|
998
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
999
|
+
"button",
|
|
1000
|
+
{
|
|
1001
|
+
type: "button",
|
|
1002
|
+
className: "sirv-filepicker__breadcrumb",
|
|
1003
|
+
onClick: () => handleNavigate("/" + breadcrumbs.slice(0, index + 1).join("/")),
|
|
1004
|
+
children: part
|
|
1005
|
+
}
|
|
1006
|
+
)
|
|
1007
|
+
] }, index))
|
|
1008
|
+
] }),
|
|
1009
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "sirv-filepicker__search", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
1010
|
+
"input",
|
|
1011
|
+
{
|
|
1012
|
+
type: "text",
|
|
1013
|
+
value: searchQuery,
|
|
1014
|
+
onChange: (e) => setSearchQuery(e.target.value),
|
|
1015
|
+
placeholder: labels.search || "Search...",
|
|
1016
|
+
className: "sirv-filepicker__search-input"
|
|
1017
|
+
}
|
|
1018
|
+
) })
|
|
1019
|
+
] }),
|
|
1020
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "sirv-filepicker__content", children: isLoading ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "sirv-filepicker__loading", children: [
|
|
1021
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "sirv-filepicker__spinner" }),
|
|
1022
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { children: labels.loading || "Loading..." })
|
|
1023
|
+
] }) : error ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "sirv-filepicker__error", children: [
|
|
1024
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { children: error }),
|
|
1025
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", onClick: () => fetchItems(currentPath), children: "Retry" })
|
|
1026
|
+
] }) : items.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "sirv-filepicker__empty", children: /* @__PURE__ */ jsxRuntime.jsx("p", { children: labels.empty || "No files found" }) }) : /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "sirv-filepicker__grid", children: [
|
|
1027
|
+
currentPath !== "/" && /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1028
|
+
"button",
|
|
1029
|
+
{
|
|
1030
|
+
type: "button",
|
|
1031
|
+
className: "sirv-filepicker__item sirv-filepicker__item--folder",
|
|
1032
|
+
onClick: handleGoUp,
|
|
1033
|
+
children: [
|
|
1034
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "sirv-filepicker__item-icon", children: /* @__PURE__ */ jsxRuntime.jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M15 18l-6-6 6-6" }) }) }),
|
|
1035
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "sirv-filepicker__item-name", children: ".." })
|
|
1036
|
+
]
|
|
1037
|
+
}
|
|
1038
|
+
),
|
|
1039
|
+
items.map((item) => {
|
|
1040
|
+
const isSelected = selectedItems.some((i) => i.path === item.path);
|
|
1041
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1042
|
+
"button",
|
|
1043
|
+
{
|
|
1044
|
+
type: "button",
|
|
1045
|
+
className: clsx2__default.default(
|
|
1046
|
+
"sirv-filepicker__item",
|
|
1047
|
+
`sirv-filepicker__item--${item.type}`,
|
|
1048
|
+
isSelected && "sirv-filepicker__item--selected"
|
|
1049
|
+
),
|
|
1050
|
+
onClick: () => handleItemClick(item),
|
|
1051
|
+
children: [
|
|
1052
|
+
item.type === "folder" ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "sirv-filepicker__item-icon", children: /* @__PURE__ */ jsxRuntime.jsx("svg", { viewBox: "0 0 24 24", fill: "currentColor", children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z" }) }) }) : item.thumbnail ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "sirv-filepicker__item-thumbnail", children: /* @__PURE__ */ jsxRuntime.jsx("img", { src: item.thumbnail, alt: "" }) }) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "sirv-filepicker__item-icon", children: /* @__PURE__ */ jsxRuntime.jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [
|
|
1053
|
+
/* @__PURE__ */ jsxRuntime.jsx("rect", { x: "3", y: "3", width: "18", height: "18", rx: "2", ry: "2" }),
|
|
1054
|
+
/* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "8.5", cy: "8.5", r: "1.5" }),
|
|
1055
|
+
/* @__PURE__ */ jsxRuntime.jsx("polyline", { points: "21 15 16 10 5 21" })
|
|
1056
|
+
] }) }),
|
|
1057
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "sirv-filepicker__item-name", title: item.name, children: item.name }),
|
|
1058
|
+
item.size && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "sirv-filepicker__item-size", children: formatFileSize(item.size) }),
|
|
1059
|
+
isSelected && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "sirv-filepicker__item-check", children: /* @__PURE__ */ jsxRuntime.jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "3", children: /* @__PURE__ */ jsxRuntime.jsx("polyline", { points: "20 6 9 17 4 12" }) }) })
|
|
1060
|
+
]
|
|
1061
|
+
},
|
|
1062
|
+
item.path
|
|
1063
|
+
);
|
|
1064
|
+
})
|
|
1065
|
+
] }) }),
|
|
1066
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "sirv-filepicker__footer", children: [
|
|
1067
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "sirv-filepicker__selection-count", children: selectedItems.length > 0 ? `${selectedItems.length} file${selectedItems.length !== 1 ? "s" : ""} selected` : "No files selected" }),
|
|
1068
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "sirv-filepicker__actions", children: [
|
|
1069
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "sirv-filepicker__btn", onClick: onClose, children: labels.cancel || "Cancel" }),
|
|
1070
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1071
|
+
"button",
|
|
1072
|
+
{
|
|
1073
|
+
type: "button",
|
|
1074
|
+
className: "sirv-filepicker__btn sirv-filepicker__btn--primary",
|
|
1075
|
+
onClick: handleSelect,
|
|
1076
|
+
disabled: selectedItems.length === 0,
|
|
1077
|
+
children: labels.select || "Select"
|
|
1078
|
+
}
|
|
1079
|
+
)
|
|
1080
|
+
] })
|
|
1081
|
+
] })
|
|
1082
|
+
] })
|
|
1083
|
+
}
|
|
1084
|
+
);
|
|
1085
|
+
}
|
|
1086
|
+
function SpreadsheetImport({
|
|
1087
|
+
onUrls,
|
|
1088
|
+
className,
|
|
1089
|
+
labels = {}
|
|
1090
|
+
}) {
|
|
1091
|
+
const [isDragOver, setIsDragOver] = react.useState(false);
|
|
1092
|
+
const [isLoading, setIsLoading] = react.useState(false);
|
|
1093
|
+
const [result, setResult] = react.useState(null);
|
|
1094
|
+
const [error, setError] = react.useState(null);
|
|
1095
|
+
const [selectedColumn, setSelectedColumn] = react.useState("");
|
|
1096
|
+
const inputRef = react.useRef(null);
|
|
1097
|
+
const processFile = react.useCallback(async (file) => {
|
|
1098
|
+
setIsLoading(true);
|
|
1099
|
+
setError(null);
|
|
1100
|
+
setResult(null);
|
|
1101
|
+
try {
|
|
1102
|
+
let parseResult;
|
|
1103
|
+
if (file.name.endsWith(".csv") || file.name.endsWith(".txt")) {
|
|
1104
|
+
const text = await file.text();
|
|
1105
|
+
parseResult = parseCsvClient(text, { previewOnly: true });
|
|
1106
|
+
} else if (file.name.endsWith(".xlsx") || file.name.endsWith(".xls")) {
|
|
1107
|
+
const buffer = await file.arrayBuffer();
|
|
1108
|
+
parseResult = await parseExcelClient(buffer, { previewOnly: true });
|
|
1109
|
+
} else {
|
|
1110
|
+
throw new Error("Unsupported file type. Please use CSV, XLSX, or TXT.");
|
|
1111
|
+
}
|
|
1112
|
+
setResult(parseResult);
|
|
1113
|
+
const maxIndex = parseResult.estimatedImageCounts.indexOf(
|
|
1114
|
+
Math.max(...parseResult.estimatedImageCounts)
|
|
1115
|
+
);
|
|
1116
|
+
if (maxIndex >= 0 && parseResult.headers[maxIndex]) {
|
|
1117
|
+
setSelectedColumn(parseResult.headers[maxIndex]);
|
|
1118
|
+
}
|
|
1119
|
+
} catch (err) {
|
|
1120
|
+
setError(err instanceof Error ? err.message : "Failed to parse file");
|
|
1121
|
+
} finally {
|
|
1122
|
+
setIsLoading(false);
|
|
1123
|
+
}
|
|
1124
|
+
}, []);
|
|
1125
|
+
const handleDragOver = react.useCallback((e) => {
|
|
1126
|
+
e.preventDefault();
|
|
1127
|
+
setIsDragOver(true);
|
|
1128
|
+
}, []);
|
|
1129
|
+
const handleDragLeave = react.useCallback((e) => {
|
|
1130
|
+
e.preventDefault();
|
|
1131
|
+
setIsDragOver(false);
|
|
1132
|
+
}, []);
|
|
1133
|
+
const handleDrop = react.useCallback(
|
|
1134
|
+
async (e) => {
|
|
1135
|
+
e.preventDefault();
|
|
1136
|
+
setIsDragOver(false);
|
|
1137
|
+
const file = e.dataTransfer.files[0];
|
|
1138
|
+
if (file && isSpreadsheetFile(file)) {
|
|
1139
|
+
await processFile(file);
|
|
1140
|
+
} else {
|
|
1141
|
+
setError("Please drop a CSV or Excel file");
|
|
1142
|
+
}
|
|
1143
|
+
},
|
|
1144
|
+
[processFile]
|
|
1145
|
+
);
|
|
1146
|
+
const handleChange = react.useCallback(
|
|
1147
|
+
async (e) => {
|
|
1148
|
+
const file = e.target.files?.[0];
|
|
1149
|
+
if (file) {
|
|
1150
|
+
await processFile(file);
|
|
1151
|
+
}
|
|
1152
|
+
e.target.value = "";
|
|
1153
|
+
},
|
|
1154
|
+
[processFile]
|
|
1155
|
+
);
|
|
1156
|
+
const handleImport = react.useCallback(async () => {
|
|
1157
|
+
if (!result || !selectedColumn) return;
|
|
1158
|
+
setIsLoading(true);
|
|
1159
|
+
try {
|
|
1160
|
+
const validUrls = result.urls.filter((u) => u.valid).map((u) => u.url);
|
|
1161
|
+
onUrls(validUrls);
|
|
1162
|
+
setResult(null);
|
|
1163
|
+
setSelectedColumn("");
|
|
1164
|
+
} catch (err) {
|
|
1165
|
+
setError(err instanceof Error ? err.message : "Failed to import URLs");
|
|
1166
|
+
} finally {
|
|
1167
|
+
setIsLoading(false);
|
|
1168
|
+
}
|
|
1169
|
+
}, [result, selectedColumn, onUrls]);
|
|
1170
|
+
const handleClear = react.useCallback(() => {
|
|
1171
|
+
setResult(null);
|
|
1172
|
+
setSelectedColumn("");
|
|
1173
|
+
setError(null);
|
|
1174
|
+
}, []);
|
|
1175
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: clsx2__default.default("sirv-spreadsheet", className), children: !result ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1176
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
1177
|
+
"div",
|
|
1178
|
+
{
|
|
1179
|
+
className: clsx2__default.default(
|
|
1180
|
+
"sirv-spreadsheet__drop",
|
|
1181
|
+
isDragOver && "sirv-spreadsheet__drop--active"
|
|
1182
|
+
),
|
|
1183
|
+
onDragOver: handleDragOver,
|
|
1184
|
+
onDragLeave: handleDragLeave,
|
|
1185
|
+
onDrop: handleDrop,
|
|
1186
|
+
onClick: () => inputRef.current?.click(),
|
|
1187
|
+
role: "button",
|
|
1188
|
+
tabIndex: 0,
|
|
1189
|
+
onKeyDown: (e) => {
|
|
1190
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
1191
|
+
inputRef.current?.click();
|
|
1192
|
+
}
|
|
1193
|
+
},
|
|
1194
|
+
children: [
|
|
1195
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1196
|
+
"input",
|
|
1197
|
+
{
|
|
1198
|
+
ref: inputRef,
|
|
1199
|
+
type: "file",
|
|
1200
|
+
accept: ".csv,.xlsx,.xls,.txt",
|
|
1201
|
+
onChange: handleChange,
|
|
1202
|
+
style: { display: "none" }
|
|
1203
|
+
}
|
|
1204
|
+
),
|
|
1205
|
+
isLoading ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "sirv-dropzone__spinner" }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1206
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
1207
|
+
"svg",
|
|
1208
|
+
{
|
|
1209
|
+
className: "sirv-spreadsheet__icon",
|
|
1210
|
+
viewBox: "0 0 24 24",
|
|
1211
|
+
fill: "none",
|
|
1212
|
+
stroke: "currentColor",
|
|
1213
|
+
strokeWidth: "2",
|
|
1214
|
+
children: [
|
|
1215
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" }),
|
|
1216
|
+
/* @__PURE__ */ jsxRuntime.jsx("polyline", { points: "14 2 14 8 20 8" }),
|
|
1217
|
+
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "16", y1: "13", x2: "8", y2: "13" }),
|
|
1218
|
+
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "16", y1: "17", x2: "8", y2: "17" }),
|
|
1219
|
+
/* @__PURE__ */ jsxRuntime.jsx("polyline", { points: "10 9 9 9 8 9" })
|
|
1220
|
+
]
|
|
1221
|
+
}
|
|
1222
|
+
),
|
|
1223
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "sirv-spreadsheet__text", children: labels.drop || "Drop CSV or Excel file here" }),
|
|
1224
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "sirv-spreadsheet__hint", children: labels.hint || "File should contain a column with image URLs" })
|
|
1225
|
+
] })
|
|
1226
|
+
]
|
|
1227
|
+
}
|
|
1228
|
+
),
|
|
1229
|
+
error && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "sirv-filelist__error", style: { padding: "8px 16px" }, children: error })
|
|
1230
|
+
] }) : /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "sirv-spreadsheet__preview", children: [
|
|
1231
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { style: { marginBottom: "12px" }, children: [
|
|
1232
|
+
/* @__PURE__ */ jsxRuntime.jsx("label", { style: { display: "block", marginBottom: "4px", fontWeight: 500 }, children: "Select URL column:" }),
|
|
1233
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
1234
|
+
"select",
|
|
1235
|
+
{
|
|
1236
|
+
value: selectedColumn,
|
|
1237
|
+
onChange: (e) => setSelectedColumn(e.target.value),
|
|
1238
|
+
style: {
|
|
1239
|
+
width: "100%",
|
|
1240
|
+
padding: "8px",
|
|
1241
|
+
borderRadius: "4px",
|
|
1242
|
+
border: "1px solid var(--sirv-border)"
|
|
1243
|
+
},
|
|
1244
|
+
children: [
|
|
1245
|
+
/* @__PURE__ */ jsxRuntime.jsx("option", { value: "", children: "Select a column" }),
|
|
1246
|
+
result.headers.map((header, i) => /* @__PURE__ */ jsxRuntime.jsxs("option", { value: header, children: [
|
|
1247
|
+
header,
|
|
1248
|
+
" (",
|
|
1249
|
+
result.estimatedImageCounts[i],
|
|
1250
|
+
" URLs)"
|
|
1251
|
+
] }, i))
|
|
1252
|
+
]
|
|
1253
|
+
}
|
|
1254
|
+
)
|
|
1255
|
+
] }),
|
|
1256
|
+
/* @__PURE__ */ jsxRuntime.jsxs("table", { className: "sirv-spreadsheet__table", children: [
|
|
1257
|
+
/* @__PURE__ */ jsxRuntime.jsx("thead", { children: /* @__PURE__ */ jsxRuntime.jsx("tr", { children: result.headers.map((header, i) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
1258
|
+
"th",
|
|
1259
|
+
{
|
|
1260
|
+
style: {
|
|
1261
|
+
background: header === selectedColumn ? "var(--sirv-primary-light)" : void 0
|
|
1262
|
+
},
|
|
1263
|
+
children: header
|
|
1264
|
+
},
|
|
1265
|
+
i
|
|
1266
|
+
)) }) }),
|
|
1267
|
+
/* @__PURE__ */ jsxRuntime.jsx("tbody", { children: result.sampleRows.slice(0, 3).map((row, i) => /* @__PURE__ */ jsxRuntime.jsx("tr", { children: row.map((cell, j) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
1268
|
+
"td",
|
|
1269
|
+
{
|
|
1270
|
+
style: {
|
|
1271
|
+
background: result.headers[j] === selectedColumn ? "var(--sirv-primary-light)" : void 0,
|
|
1272
|
+
maxWidth: "200px",
|
|
1273
|
+
overflow: "hidden",
|
|
1274
|
+
textOverflow: "ellipsis",
|
|
1275
|
+
whiteSpace: "nowrap"
|
|
1276
|
+
},
|
|
1277
|
+
children: cell
|
|
1278
|
+
},
|
|
1279
|
+
j
|
|
1280
|
+
)) }, i)) })
|
|
1281
|
+
] }),
|
|
1282
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "sirv-spreadsheet__stats", children: [
|
|
1283
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
|
|
1284
|
+
"Total rows: ",
|
|
1285
|
+
result.rowCount
|
|
1286
|
+
] }),
|
|
1287
|
+
selectedColumn && /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "sirv-spreadsheet__stat--valid", children: [
|
|
1288
|
+
labels.validUrls || "Valid URLs",
|
|
1289
|
+
":",
|
|
1290
|
+
" ",
|
|
1291
|
+
result.estimatedImageCounts[result.headers.indexOf(selectedColumn)] || 0
|
|
1292
|
+
] }) })
|
|
1293
|
+
] }),
|
|
1294
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", gap: "8px", marginTop: "12px" }, children: [
|
|
1295
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "sirv-btn", onClick: handleClear, children: labels.clear || "Clear" }),
|
|
1296
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1297
|
+
"button",
|
|
1298
|
+
{
|
|
1299
|
+
type: "button",
|
|
1300
|
+
className: "sirv-btn sirv-btn--primary",
|
|
1301
|
+
onClick: handleImport,
|
|
1302
|
+
disabled: !selectedColumn || isLoading,
|
|
1303
|
+
children: isLoading ? "Importing..." : labels.import || "Import URLs"
|
|
1304
|
+
}
|
|
1305
|
+
)
|
|
1306
|
+
] })
|
|
1307
|
+
] }) });
|
|
1308
|
+
}
|
|
1309
|
+
function useSirvUpload(options) {
|
|
1310
|
+
const {
|
|
1311
|
+
presignEndpoint,
|
|
1312
|
+
proxyEndpoint,
|
|
1313
|
+
folder,
|
|
1314
|
+
onConflict,
|
|
1315
|
+
concurrency,
|
|
1316
|
+
autoUpload,
|
|
1317
|
+
onUpload,
|
|
1318
|
+
onError
|
|
1319
|
+
} = options;
|
|
1320
|
+
const [files, setFiles] = react.useState([]);
|
|
1321
|
+
const abortControllers = react.useRef(/* @__PURE__ */ new Map());
|
|
1322
|
+
const uploadQueue = react.useRef([]);
|
|
1323
|
+
const activeUploads = react.useRef(0);
|
|
1324
|
+
const updateFile = react.useCallback((id, updates) => {
|
|
1325
|
+
setFiles(
|
|
1326
|
+
(prev) => prev.map((f) => f.id === id ? { ...f, ...updates } : f)
|
|
1327
|
+
);
|
|
1328
|
+
}, []);
|
|
1329
|
+
const uploadWithPresign = react.useCallback(
|
|
1330
|
+
async (file, signal) => {
|
|
1331
|
+
if (!presignEndpoint) throw new Error("No presign endpoint configured");
|
|
1332
|
+
if (!file.file) throw new Error("No file data");
|
|
1333
|
+
const presignRes = await fetch(presignEndpoint, {
|
|
1334
|
+
method: "POST",
|
|
1335
|
+
headers: { "Content-Type": "application/json" },
|
|
1336
|
+
body: JSON.stringify({
|
|
1337
|
+
filename: file.filename,
|
|
1338
|
+
contentType: getMimeType(file.file),
|
|
1339
|
+
folder,
|
|
1340
|
+
size: file.file.size
|
|
1341
|
+
}),
|
|
1342
|
+
signal
|
|
1343
|
+
});
|
|
1344
|
+
if (!presignRes.ok) {
|
|
1345
|
+
const err = await presignRes.json().catch(() => ({}));
|
|
1346
|
+
throw new Error(err.error || `Failed to get upload URL: ${presignRes.status}`);
|
|
1347
|
+
}
|
|
1348
|
+
const { uploadUrl, publicUrl, path, error } = await presignRes.json();
|
|
1349
|
+
if (error) throw new Error(error);
|
|
1350
|
+
if (!uploadUrl) throw new Error("No upload URL returned");
|
|
1351
|
+
updateFile(file.id, { status: "uploading", progress: 10 });
|
|
1352
|
+
const uploadRes = await fetch(uploadUrl, {
|
|
1353
|
+
method: "PUT",
|
|
1354
|
+
body: file.file,
|
|
1355
|
+
headers: {
|
|
1356
|
+
"Content-Type": getMimeType(file.file)
|
|
1357
|
+
},
|
|
1358
|
+
signal
|
|
1359
|
+
});
|
|
1360
|
+
if (!uploadRes.ok) {
|
|
1361
|
+
throw new Error(`Upload failed: ${uploadRes.status}`);
|
|
1362
|
+
}
|
|
1363
|
+
updateFile(file.id, {
|
|
1364
|
+
status: "success",
|
|
1365
|
+
progress: 100,
|
|
1366
|
+
sirvUrl: publicUrl,
|
|
1367
|
+
sirvPath: path
|
|
1368
|
+
});
|
|
1369
|
+
},
|
|
1370
|
+
[presignEndpoint, folder, updateFile]
|
|
1371
|
+
);
|
|
1372
|
+
const uploadWithProxy = react.useCallback(
|
|
1373
|
+
async (file, signal) => {
|
|
1374
|
+
if (!proxyEndpoint) throw new Error("No proxy endpoint configured");
|
|
1375
|
+
if (!file.file) throw new Error("No file data");
|
|
1376
|
+
updateFile(file.id, { status: "uploading", progress: 10 });
|
|
1377
|
+
const arrayBuffer = await file.file.arrayBuffer();
|
|
1378
|
+
const base64 = btoa(
|
|
1379
|
+
new Uint8Array(arrayBuffer).reduce((data, byte) => data + String.fromCharCode(byte), "")
|
|
1380
|
+
);
|
|
1381
|
+
updateFile(file.id, { progress: 30 });
|
|
1382
|
+
const res = await fetch(`${proxyEndpoint}/upload`, {
|
|
1383
|
+
method: "POST",
|
|
1384
|
+
headers: { "Content-Type": "application/json" },
|
|
1385
|
+
body: JSON.stringify({
|
|
1386
|
+
data: base64,
|
|
1387
|
+
filename: file.filename,
|
|
1388
|
+
folder,
|
|
1389
|
+
contentType: getMimeType(file.file),
|
|
1390
|
+
onConflict: onConflict === "ask" ? "rename" : onConflict
|
|
1391
|
+
}),
|
|
1392
|
+
signal
|
|
1393
|
+
});
|
|
1394
|
+
updateFile(file.id, { progress: 80 });
|
|
1395
|
+
if (!res.ok) {
|
|
1396
|
+
const err = await res.json().catch(() => ({}));
|
|
1397
|
+
throw new Error(err.error || `Upload failed: ${res.status}`);
|
|
1398
|
+
}
|
|
1399
|
+
const result = await res.json();
|
|
1400
|
+
if (!result.success) {
|
|
1401
|
+
throw new Error(result.error || "Upload failed");
|
|
1402
|
+
}
|
|
1403
|
+
updateFile(file.id, {
|
|
1404
|
+
status: "success",
|
|
1405
|
+
progress: 100,
|
|
1406
|
+
sirvUrl: result.url,
|
|
1407
|
+
sirvPath: result.path
|
|
1408
|
+
});
|
|
1409
|
+
},
|
|
1410
|
+
[proxyEndpoint, folder, onConflict, updateFile]
|
|
1411
|
+
);
|
|
1412
|
+
const uploadFile = react.useCallback(
|
|
1413
|
+
async (id) => {
|
|
1414
|
+
const file = files.find((f) => f.id === id);
|
|
1415
|
+
if (!file || file.status === "uploading" || file.status === "success") return;
|
|
1416
|
+
const controller = new AbortController();
|
|
1417
|
+
abortControllers.current.set(id, controller);
|
|
1418
|
+
try {
|
|
1419
|
+
updateFile(id, { status: "uploading", progress: 0, error: void 0 });
|
|
1420
|
+
if (presignEndpoint) {
|
|
1421
|
+
await uploadWithPresign(file, controller.signal);
|
|
1422
|
+
} else if (proxyEndpoint) {
|
|
1423
|
+
await uploadWithProxy(file, controller.signal);
|
|
1424
|
+
} else {
|
|
1425
|
+
throw new Error("No upload endpoint configured");
|
|
1426
|
+
}
|
|
1427
|
+
const updatedFile = files.find((f) => f.id === id);
|
|
1428
|
+
if (updatedFile && onUpload) {
|
|
1429
|
+
onUpload([{ ...updatedFile, status: "success" }]);
|
|
1430
|
+
}
|
|
1431
|
+
} catch (err) {
|
|
1432
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
1433
|
+
updateFile(id, { status: "pending", progress: 0 });
|
|
1434
|
+
return;
|
|
1435
|
+
}
|
|
1436
|
+
const errorMsg = err instanceof Error ? err.message : "Upload failed";
|
|
1437
|
+
updateFile(id, { status: "error", progress: 0, error: errorMsg });
|
|
1438
|
+
onError?.(errorMsg, file);
|
|
1439
|
+
} finally {
|
|
1440
|
+
abortControllers.current.delete(id);
|
|
1441
|
+
activeUploads.current--;
|
|
1442
|
+
processQueue();
|
|
1443
|
+
}
|
|
1444
|
+
},
|
|
1445
|
+
[files, presignEndpoint, proxyEndpoint, uploadWithPresign, uploadWithProxy, updateFile, onUpload, onError]
|
|
1446
|
+
);
|
|
1447
|
+
const processQueue = react.useCallback(() => {
|
|
1448
|
+
while (activeUploads.current < concurrency && uploadQueue.current.length > 0) {
|
|
1449
|
+
const id = uploadQueue.current.shift();
|
|
1450
|
+
if (id) {
|
|
1451
|
+
activeUploads.current++;
|
|
1452
|
+
uploadFile(id);
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
}, [concurrency, uploadFile]);
|
|
1456
|
+
const uploadAll = react.useCallback(async () => {
|
|
1457
|
+
const pendingFiles = files.filter((f) => f.status === "pending" || f.status === "error");
|
|
1458
|
+
uploadQueue.current = pendingFiles.map((f) => f.id);
|
|
1459
|
+
processQueue();
|
|
1460
|
+
}, [files, processQueue]);
|
|
1461
|
+
const addFiles = react.useCallback(
|
|
1462
|
+
(newFiles) => {
|
|
1463
|
+
setFiles((prev) => [...prev, ...newFiles]);
|
|
1464
|
+
if (autoUpload) {
|
|
1465
|
+
uploadQueue.current.push(...newFiles.map((f) => f.id));
|
|
1466
|
+
processQueue();
|
|
1467
|
+
}
|
|
1468
|
+
},
|
|
1469
|
+
[autoUpload, processQueue]
|
|
1470
|
+
);
|
|
1471
|
+
const addUrls = react.useCallback(
|
|
1472
|
+
(urls) => {
|
|
1473
|
+
const newFiles = urls.map((url) => {
|
|
1474
|
+
const filename = url.split("/").pop() || "image.jpg";
|
|
1475
|
+
return {
|
|
1476
|
+
id: generateId(),
|
|
1477
|
+
filename,
|
|
1478
|
+
previewUrl: url,
|
|
1479
|
+
sirvUrl: url,
|
|
1480
|
+
status: "success",
|
|
1481
|
+
progress: 100
|
|
1482
|
+
};
|
|
1483
|
+
});
|
|
1484
|
+
setFiles((prev) => [...prev, ...newFiles]);
|
|
1485
|
+
},
|
|
1486
|
+
[]
|
|
1487
|
+
);
|
|
1488
|
+
const removeFile = react.useCallback((id) => {
|
|
1489
|
+
const controller = abortControllers.current.get(id);
|
|
1490
|
+
if (controller) {
|
|
1491
|
+
controller.abort();
|
|
1492
|
+
}
|
|
1493
|
+
uploadQueue.current = uploadQueue.current.filter((qid) => qid !== id);
|
|
1494
|
+
setFiles((prev) => prev.filter((f) => f.id !== id));
|
|
1495
|
+
}, []);
|
|
1496
|
+
const clearFiles = react.useCallback(() => {
|
|
1497
|
+
abortControllers.current.forEach((controller) => controller.abort());
|
|
1498
|
+
abortControllers.current.clear();
|
|
1499
|
+
uploadQueue.current = [];
|
|
1500
|
+
activeUploads.current = 0;
|
|
1501
|
+
setFiles([]);
|
|
1502
|
+
}, []);
|
|
1503
|
+
const retryFile = react.useCallback(
|
|
1504
|
+
async (id) => {
|
|
1505
|
+
uploadQueue.current.push(id);
|
|
1506
|
+
processQueue();
|
|
1507
|
+
},
|
|
1508
|
+
[processQueue]
|
|
1509
|
+
);
|
|
1510
|
+
const cancelUpload = react.useCallback((id) => {
|
|
1511
|
+
const controller = abortControllers.current.get(id);
|
|
1512
|
+
if (controller) {
|
|
1513
|
+
controller.abort();
|
|
1514
|
+
}
|
|
1515
|
+
}, []);
|
|
1516
|
+
const progress = files.length > 0 ? Math.round(files.reduce((sum, f) => sum + f.progress, 0) / files.length) : 0;
|
|
1517
|
+
const isUploading = files.some((f) => f.status === "uploading" || f.status === "processing");
|
|
1518
|
+
const isComplete = files.length > 0 && files.every((f) => f.status === "success");
|
|
1519
|
+
return {
|
|
1520
|
+
files,
|
|
1521
|
+
addFiles,
|
|
1522
|
+
addUrls,
|
|
1523
|
+
removeFile,
|
|
1524
|
+
clearFiles,
|
|
1525
|
+
uploadAll,
|
|
1526
|
+
uploadFile,
|
|
1527
|
+
retryFile,
|
|
1528
|
+
cancelUpload,
|
|
1529
|
+
progress,
|
|
1530
|
+
isUploading,
|
|
1531
|
+
isComplete
|
|
1532
|
+
};
|
|
1533
|
+
}
|
|
1534
|
+
var DEFAULT_LABELS = {
|
|
1535
|
+
dropzone: "Drop files here or click to browse",
|
|
1536
|
+
dropzoneHint: "Supports JPG, PNG, WebP, GIF, HEIC up to 10MB",
|
|
1537
|
+
browse: "Browse",
|
|
1538
|
+
uploadFiles: "Upload Files",
|
|
1539
|
+
importUrls: "Import URLs",
|
|
1540
|
+
selectFromSirv: "Select from Sirv",
|
|
1541
|
+
uploading: "Uploading...",
|
|
1542
|
+
processing: "Processing...",
|
|
1543
|
+
success: "Uploaded",
|
|
1544
|
+
error: "Failed",
|
|
1545
|
+
retry: "Retry",
|
|
1546
|
+
remove: "Remove",
|
|
1547
|
+
cancel: "Cancel",
|
|
1548
|
+
overwrite: "Overwrite",
|
|
1549
|
+
rename: "Rename",
|
|
1550
|
+
skip: "Skip",
|
|
1551
|
+
conflictTitle: "File exists",
|
|
1552
|
+
conflictMessage: "A file with this name already exists."
|
|
1553
|
+
};
|
|
1554
|
+
function SirvUploader({
|
|
1555
|
+
presignEndpoint,
|
|
1556
|
+
proxyEndpoint,
|
|
1557
|
+
sirvAccount,
|
|
1558
|
+
folder = "/",
|
|
1559
|
+
onUpload,
|
|
1560
|
+
onError,
|
|
1561
|
+
onSelect,
|
|
1562
|
+
onRemove,
|
|
1563
|
+
features = {},
|
|
1564
|
+
maxFiles = 50,
|
|
1565
|
+
maxFileSize = 10 * 1024 * 1024,
|
|
1566
|
+
accept = ["image/*"],
|
|
1567
|
+
onConflict = "rename",
|
|
1568
|
+
autoUpload = true,
|
|
1569
|
+
concurrency = 3,
|
|
1570
|
+
className,
|
|
1571
|
+
disabled = false,
|
|
1572
|
+
compact = false,
|
|
1573
|
+
labels: customLabels = {},
|
|
1574
|
+
children
|
|
1575
|
+
}) {
|
|
1576
|
+
const labels = { ...DEFAULT_LABELS, ...customLabels };
|
|
1577
|
+
const {
|
|
1578
|
+
batch = true,
|
|
1579
|
+
csvImport = true,
|
|
1580
|
+
filePicker = true,
|
|
1581
|
+
dragDrop = true
|
|
1582
|
+
} = features;
|
|
1583
|
+
const [activeTab, setActiveTab] = react.useState("upload");
|
|
1584
|
+
const [isPickerOpen, setIsPickerOpen] = react.useState(false);
|
|
1585
|
+
if (!presignEndpoint && !proxyEndpoint) {
|
|
1586
|
+
console.warn("SirvUploader: Either presignEndpoint or proxyEndpoint must be provided");
|
|
1587
|
+
}
|
|
1588
|
+
const upload = useSirvUpload({
|
|
1589
|
+
presignEndpoint,
|
|
1590
|
+
proxyEndpoint,
|
|
1591
|
+
folder,
|
|
1592
|
+
onConflict,
|
|
1593
|
+
concurrency,
|
|
1594
|
+
autoUpload,
|
|
1595
|
+
onUpload,
|
|
1596
|
+
onError
|
|
1597
|
+
});
|
|
1598
|
+
const handleFiles = react.useCallback(
|
|
1599
|
+
(files) => {
|
|
1600
|
+
upload.addFiles(files);
|
|
1601
|
+
onSelect?.(files);
|
|
1602
|
+
},
|
|
1603
|
+
[upload, onSelect]
|
|
1604
|
+
);
|
|
1605
|
+
const handleSpreadsheet = react.useCallback(() => {
|
|
1606
|
+
setActiveTab("urls");
|
|
1607
|
+
}, []);
|
|
1608
|
+
const handleUrls = react.useCallback(
|
|
1609
|
+
(urls) => {
|
|
1610
|
+
upload.addUrls(urls);
|
|
1611
|
+
},
|
|
1612
|
+
[upload]
|
|
1613
|
+
);
|
|
1614
|
+
const handlePickerSelect = react.useCallback(
|
|
1615
|
+
(items) => {
|
|
1616
|
+
const files = items.map((item) => ({
|
|
1617
|
+
id: generateId(),
|
|
1618
|
+
filename: item.name,
|
|
1619
|
+
previewUrl: item.thumbnail || "",
|
|
1620
|
+
sirvUrl: `https://${sirvAccount}.sirv.com${item.path}`,
|
|
1621
|
+
sirvPath: item.path,
|
|
1622
|
+
size: item.size,
|
|
1623
|
+
status: "success",
|
|
1624
|
+
progress: 100
|
|
1625
|
+
}));
|
|
1626
|
+
upload.addFiles(files);
|
|
1627
|
+
onSelect?.(files);
|
|
1628
|
+
},
|
|
1629
|
+
[sirvAccount, upload, onSelect]
|
|
1630
|
+
);
|
|
1631
|
+
const handleRemove = react.useCallback(
|
|
1632
|
+
(id) => {
|
|
1633
|
+
const file = upload.files.find((f) => f.id === id);
|
|
1634
|
+
upload.removeFile(id);
|
|
1635
|
+
if (file) onRemove?.(file);
|
|
1636
|
+
},
|
|
1637
|
+
[upload, onRemove]
|
|
1638
|
+
);
|
|
1639
|
+
const handleUploadAll = react.useCallback(() => {
|
|
1640
|
+
upload.uploadAll();
|
|
1641
|
+
}, [upload]);
|
|
1642
|
+
const handleClearAll = react.useCallback(() => {
|
|
1643
|
+
upload.clearFiles();
|
|
1644
|
+
}, [upload]);
|
|
1645
|
+
const hasFiles = upload.files.length > 0;
|
|
1646
|
+
const hasPendingFiles = upload.files.some((f) => f.status === "pending" || f.status === "error");
|
|
1647
|
+
const showTabs = csvImport && batch;
|
|
1648
|
+
const browseEndpoint = proxyEndpoint || (presignEndpoint ? presignEndpoint.replace(/\/presign$/, "") : "");
|
|
1649
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: clsx2__default.default("sirv-uploader", className), children: [
|
|
1650
|
+
showTabs && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "sirv-tabs", children: [
|
|
1651
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1652
|
+
"button",
|
|
1653
|
+
{
|
|
1654
|
+
type: "button",
|
|
1655
|
+
className: clsx2__default.default("sirv-tabs__tab", activeTab === "upload" && "sirv-tabs__tab--active"),
|
|
1656
|
+
onClick: () => setActiveTab("upload"),
|
|
1657
|
+
children: labels.uploadFiles
|
|
1658
|
+
}
|
|
1659
|
+
),
|
|
1660
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1661
|
+
"button",
|
|
1662
|
+
{
|
|
1663
|
+
type: "button",
|
|
1664
|
+
className: clsx2__default.default("sirv-tabs__tab", activeTab === "urls" && "sirv-tabs__tab--active"),
|
|
1665
|
+
onClick: () => setActiveTab("urls"),
|
|
1666
|
+
children: labels.importUrls
|
|
1667
|
+
}
|
|
1668
|
+
)
|
|
1669
|
+
] }),
|
|
1670
|
+
activeTab === "upload" && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1671
|
+
dragDrop && /* @__PURE__ */ jsxRuntime.jsx(
|
|
1672
|
+
DropZone,
|
|
1673
|
+
{
|
|
1674
|
+
onFiles: handleFiles,
|
|
1675
|
+
onSpreadsheet: csvImport ? handleSpreadsheet : void 0,
|
|
1676
|
+
accept,
|
|
1677
|
+
maxFiles: batch ? maxFiles : 1,
|
|
1678
|
+
maxFileSize,
|
|
1679
|
+
disabled,
|
|
1680
|
+
compact,
|
|
1681
|
+
labels: {
|
|
1682
|
+
dropzone: labels.dropzone,
|
|
1683
|
+
dropzoneHint: labels.dropzoneHint,
|
|
1684
|
+
browse: labels.browse
|
|
1685
|
+
},
|
|
1686
|
+
children
|
|
1687
|
+
}
|
|
1688
|
+
),
|
|
1689
|
+
hasFiles && /* @__PURE__ */ jsxRuntime.jsx(
|
|
1690
|
+
FileList,
|
|
1691
|
+
{
|
|
1692
|
+
files: upload.files,
|
|
1693
|
+
onRemove: handleRemove,
|
|
1694
|
+
onRetry: upload.retryFile,
|
|
1695
|
+
labels: {
|
|
1696
|
+
retry: labels.retry,
|
|
1697
|
+
remove: labels.remove,
|
|
1698
|
+
uploading: labels.uploading,
|
|
1699
|
+
processing: labels.processing,
|
|
1700
|
+
success: labels.success,
|
|
1701
|
+
error: labels.error
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
)
|
|
1705
|
+
] }),
|
|
1706
|
+
activeTab === "urls" && csvImport && /* @__PURE__ */ jsxRuntime.jsx(SpreadsheetImport, { onUrls: handleUrls }),
|
|
1707
|
+
(hasFiles || filePicker) && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "sirv-uploader__toolbar", children: [
|
|
1708
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "sirv-uploader__toolbar-left", children: filePicker && browseEndpoint && /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1709
|
+
"button",
|
|
1710
|
+
{
|
|
1711
|
+
type: "button",
|
|
1712
|
+
className: "sirv-btn",
|
|
1713
|
+
onClick: () => setIsPickerOpen(true),
|
|
1714
|
+
disabled,
|
|
1715
|
+
children: [
|
|
1716
|
+
/* @__PURE__ */ jsxRuntime.jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z" }) }),
|
|
1717
|
+
labels.selectFromSirv
|
|
1718
|
+
]
|
|
1719
|
+
}
|
|
1720
|
+
) }),
|
|
1721
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "sirv-uploader__toolbar-right", children: [
|
|
1722
|
+
hasFiles && /* @__PURE__ */ jsxRuntime.jsx(
|
|
1723
|
+
"button",
|
|
1724
|
+
{
|
|
1725
|
+
type: "button",
|
|
1726
|
+
className: "sirv-btn",
|
|
1727
|
+
onClick: handleClearAll,
|
|
1728
|
+
disabled: disabled || upload.isUploading,
|
|
1729
|
+
children: "Clear All"
|
|
1730
|
+
}
|
|
1731
|
+
),
|
|
1732
|
+
hasPendingFiles && !autoUpload && /* @__PURE__ */ jsxRuntime.jsx(
|
|
1733
|
+
"button",
|
|
1734
|
+
{
|
|
1735
|
+
type: "button",
|
|
1736
|
+
className: "sirv-btn sirv-btn--primary",
|
|
1737
|
+
onClick: handleUploadAll,
|
|
1738
|
+
disabled: disabled || upload.isUploading,
|
|
1739
|
+
children: upload.isUploading ? labels.uploading : "Upload All"
|
|
1740
|
+
}
|
|
1741
|
+
)
|
|
1742
|
+
] })
|
|
1743
|
+
] }),
|
|
1744
|
+
hasFiles && /* @__PURE__ */ jsxRuntime.jsx(FileListSummary, { files: upload.files }),
|
|
1745
|
+
filePicker && browseEndpoint && /* @__PURE__ */ jsxRuntime.jsx(
|
|
1746
|
+
FilePicker,
|
|
1747
|
+
{
|
|
1748
|
+
endpoint: browseEndpoint,
|
|
1749
|
+
isOpen: isPickerOpen,
|
|
1750
|
+
onClose: () => setIsPickerOpen(false),
|
|
1751
|
+
onSelect: handlePickerSelect,
|
|
1752
|
+
multiple: batch,
|
|
1753
|
+
initialPath: folder,
|
|
1754
|
+
labels: {
|
|
1755
|
+
title: labels.selectFromSirv,
|
|
1756
|
+
cancel: labels.cancel
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
)
|
|
1760
|
+
] });
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1763
|
+
exports.ACCEPTED_IMAGE_FORMATS = ACCEPTED_IMAGE_FORMATS;
|
|
1764
|
+
exports.DEFAULT_MAX_FILE_SIZE = DEFAULT_MAX_FILE_SIZE;
|
|
1765
|
+
exports.DropZone = DropZone;
|
|
1766
|
+
exports.FileList = FileList;
|
|
1767
|
+
exports.FileListSummary = FileListSummary;
|
|
1768
|
+
exports.FilePicker = FilePicker;
|
|
1769
|
+
exports.SirvUploader = SirvUploader;
|
|
1770
|
+
exports.SpreadsheetImport = SpreadsheetImport;
|
|
1771
|
+
exports.convertHeicWithFallback = convertHeicWithFallback;
|
|
1772
|
+
exports.defaultUrlValidator = defaultUrlValidator;
|
|
1773
|
+
exports.detectDelimiter = detectDelimiter;
|
|
1774
|
+
exports.formatFileSize = formatFileSize;
|
|
1775
|
+
exports.generateId = generateId;
|
|
1776
|
+
exports.getImageDimensions = getImageDimensions;
|
|
1777
|
+
exports.getMimeType = getMimeType;
|
|
1778
|
+
exports.isHeifFile = isHeifFile;
|
|
1779
|
+
exports.isImageFile = isImageFile;
|
|
1780
|
+
exports.isSpreadsheetFile = isSpreadsheetFile;
|
|
1781
|
+
exports.parseCsvClient = parseCsvClient;
|
|
1782
|
+
exports.parseExcelClient = parseExcelClient;
|
|
1783
|
+
exports.sirvUrlValidator = sirvUrlValidator;
|
|
1784
|
+
exports.useSirvUpload = useSirvUpload;
|
|
1785
|
+
exports.validateFileSize = validateFileSize;
|
|
1786
|
+
//# sourceMappingURL=index.js.map
|
|
1787
|
+
//# sourceMappingURL=index.js.map
|