squarefi-bff-api-module 1.30.9 → 1.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,395 @@
1
+ /**
2
+ * Ready-to-use File Manager component
3
+ *
4
+ * Copy this file to your project and use it!
5
+ *
6
+ * Installation:
7
+ * npm install squarefi-bff-api-module
8
+ *
9
+ * Usage:
10
+ * import { FileManager } from './FileManager';
11
+ *
12
+ * <FileManager userId="user-123" />
13
+ */
14
+
15
+ import React, { useState } from 'react';
16
+ import {
17
+ useFileUpload,
18
+ useUserFiles,
19
+ DEFAULT_BUCKET,
20
+ } from 'squarefi-bff-api-module';
21
+
22
+ interface FileManagerProps {
23
+ userId: string;
24
+ bucket?: string;
25
+ maxFileSize?: number; // in bytes
26
+ allowedTypes?: string[];
27
+ onFileUpload?: (path: string) => void;
28
+ onFileDelete?: (fileName: string) => void;
29
+ }
30
+
31
+ /**
32
+ * Universal component for managing user files
33
+ */
34
+ export const FileManager: React.FC<FileManagerProps> = ({
35
+ userId,
36
+ bucket = DEFAULT_BUCKET,
37
+ maxFileSize = 10 * 1024 * 1024, // 10MB by default
38
+ allowedTypes = ['*/*'], // all types by default
39
+ onFileUpload,
40
+ onFileDelete,
41
+ }) => {
42
+ const [selectedFile, setSelectedFile] = useState<File | null>(null);
43
+ const [validationError, setValidationError] = useState<string | null>(null);
44
+
45
+ // File upload hook
46
+ const { upload, uploading, progress, error: uploadError } = useFileUpload({
47
+ userId,
48
+ bucket,
49
+ onSuccess: (result) => {
50
+ setSelectedFile(null);
51
+ setValidationError(null);
52
+ if (result.path) {
53
+ onFileUpload?.(result.path);
54
+ }
55
+ reload(); // Refresh list after upload
56
+ },
57
+ });
58
+
59
+ // File list hook
60
+ const {
61
+ files,
62
+ loading,
63
+ error: listError,
64
+ reload,
65
+ deleteOne,
66
+ } = useUserFiles({
67
+ userId,
68
+ bucket,
69
+ autoLoad: true,
70
+ autoGenerateUrls: true,
71
+ urlExpiresIn: 3600,
72
+ });
73
+
74
+ // File validation
75
+ const validateFile = (file: File): string | null => {
76
+ // Size check
77
+ if (file.size > maxFileSize) {
78
+ return `File too large. Maximum ${(maxFileSize / 1024 / 1024).toFixed(0)}MB`;
79
+ }
80
+
81
+ // Type check
82
+ if (!allowedTypes.includes('*/*')) {
83
+ const isAllowed = allowedTypes.some((type) => {
84
+ if (type.endsWith('/*')) {
85
+ // Example: image/*
86
+ const prefix = type.split('/')[0];
87
+ return file.type.startsWith(prefix + '/');
88
+ }
89
+ return file.type === type;
90
+ });
91
+
92
+ if (!isAllowed) {
93
+ return `Unsupported file type. Allowed: ${allowedTypes.join(', ')}`;
94
+ }
95
+ }
96
+
97
+ return null;
98
+ };
99
+
100
+ const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
101
+ const file = e.target.files?.[0];
102
+ if (!file) return;
103
+
104
+ const error = validateFile(file);
105
+ if (error) {
106
+ setValidationError(error);
107
+ setSelectedFile(null);
108
+ return;
109
+ }
110
+
111
+ setValidationError(null);
112
+ setSelectedFile(file);
113
+ };
114
+
115
+ const handleUpload = async () => {
116
+ if (!selectedFile) return;
117
+
118
+ const timestamp = Date.now();
119
+ const uniqueFileName = `${timestamp}-${selectedFile.name}`;
120
+ await upload(selectedFile, uniqueFileName);
121
+ };
122
+
123
+ const handleDelete = async (fileName: string) => {
124
+ const confirmed = window.confirm(`Are you sure you want to delete "${fileName}"?`);
125
+ if (!confirmed) return;
126
+
127
+ const success = await deleteOne(fileName);
128
+ if (success) {
129
+ onFileDelete?.(fileName);
130
+ } else {
131
+ alert('Failed to delete file');
132
+ }
133
+ };
134
+
135
+ const formatFileSize = (bytes: number): string => {
136
+ if (bytes < 1024) return bytes + ' B';
137
+ if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
138
+ return (bytes / 1024 / 1024).toFixed(1) + ' MB';
139
+ };
140
+
141
+ const formatDate = (dateString: string): string => {
142
+ const date = new Date(dateString);
143
+ return date.toLocaleDateString('en-US', {
144
+ year: 'numeric',
145
+ month: 'short',
146
+ day: 'numeric',
147
+ hour: '2-digit',
148
+ minute: '2-digit',
149
+ });
150
+ };
151
+
152
+ return (
153
+ <div style={styles.container}>
154
+ <h2 style={styles.title}>File Management</h2>
155
+
156
+ {/* Upload section */}
157
+ <div style={styles.uploadSection}>
158
+ <h3 style={styles.sectionTitle}>📤 Upload File</h3>
159
+
160
+ <input
161
+ type="file"
162
+ onChange={handleFileSelect}
163
+ disabled={uploading}
164
+ style={styles.fileInput}
165
+ accept={allowedTypes.join(',')}
166
+ />
167
+
168
+ {selectedFile && !validationError && (
169
+ <div style={styles.selectedFile}>
170
+ <p style={styles.fileName}>
171
+ 📄 {selectedFile.name} ({formatFileSize(selectedFile.size)})
172
+ </p>
173
+ <button
174
+ onClick={handleUpload}
175
+ disabled={uploading}
176
+ style={{
177
+ ...styles.button,
178
+ ...styles.uploadButton,
179
+ ...(uploading ? styles.buttonDisabled : {}),
180
+ }}
181
+ >
182
+ {uploading ? `⏳ Uploading... ${progress}%` : '✅ Upload'}
183
+ </button>
184
+ </div>
185
+ )}
186
+
187
+ {validationError && (
188
+ <p style={styles.errorText}>❌ {validationError}</p>
189
+ )}
190
+
191
+ {uploadError && (
192
+ <p style={styles.errorText}>❌ Upload error: {uploadError}</p>
193
+ )}
194
+ </div>
195
+
196
+ {/* File list section */}
197
+ <div style={styles.listSection}>
198
+ <div style={styles.listHeader}>
199
+ <h3 style={styles.sectionTitle}>
200
+ 📁 Your Files ({files.length})
201
+ </h3>
202
+ <button
203
+ onClick={reload}
204
+ disabled={loading}
205
+ style={{
206
+ ...styles.button,
207
+ ...styles.refreshButton,
208
+ ...(loading ? styles.buttonDisabled : {}),
209
+ }}
210
+ >
211
+ 🔄 Refresh
212
+ </button>
213
+ </div>
214
+
215
+ {loading && <p style={styles.loadingText}>Loading file list...</p>}
216
+
217
+ {listError && (
218
+ <p style={styles.errorText}>❌ List loading error: {listError}</p>
219
+ )}
220
+
221
+ {!loading && files.length === 0 && (
222
+ <p style={styles.emptyText}>You don't have any uploaded files yet</p>
223
+ )}
224
+
225
+ {files.length > 0 && (
226
+ <div style={styles.filesList}>
227
+ {files.map((file) => (
228
+ <div key={file.id} style={styles.fileItem}>
229
+ <div style={styles.fileInfo}>
230
+ <a
231
+ href={file.signedUrl}
232
+ target="_blank"
233
+ rel="noopener noreferrer"
234
+ style={styles.fileLink}
235
+ >
236
+ 📄 {file.name}
237
+ </a>
238
+ <p style={styles.fileDate}>
239
+ {formatDate(file.created_at)}
240
+ </p>
241
+ </div>
242
+ <button
243
+ onClick={() => handleDelete(file.name)}
244
+ style={{
245
+ ...styles.button,
246
+ ...styles.deleteButton,
247
+ }}
248
+ >
249
+ 🗑️ Delete
250
+ </button>
251
+ </div>
252
+ ))}
253
+ </div>
254
+ )}
255
+ </div>
256
+ </div>
257
+ );
258
+ };
259
+
260
+ // Стили компонента
261
+ const styles: { [key: string]: React.CSSProperties } = {
262
+ container: {
263
+ padding: '20px',
264
+ maxWidth: '900px',
265
+ margin: '0 auto',
266
+ fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
267
+ },
268
+ title: {
269
+ fontSize: '28px',
270
+ fontWeight: 'bold',
271
+ marginBottom: '30px',
272
+ color: '#333',
273
+ },
274
+ uploadSection: {
275
+ marginBottom: '40px',
276
+ padding: '25px',
277
+ border: '2px solid #e0e0e0',
278
+ borderRadius: '12px',
279
+ backgroundColor: '#fafafa',
280
+ },
281
+ listSection: {
282
+ padding: '25px',
283
+ border: '2px solid #e0e0e0',
284
+ borderRadius: '12px',
285
+ backgroundColor: '#ffffff',
286
+ },
287
+ sectionTitle: {
288
+ fontSize: '20px',
289
+ fontWeight: '600',
290
+ marginBottom: '15px',
291
+ color: '#555',
292
+ },
293
+ fileInput: {
294
+ padding: '10px',
295
+ fontSize: '16px',
296
+ border: '2px solid #ddd',
297
+ borderRadius: '8px',
298
+ width: '100%',
299
+ cursor: 'pointer',
300
+ },
301
+ selectedFile: {
302
+ marginTop: '15px',
303
+ padding: '15px',
304
+ backgroundColor: '#e8f5e9',
305
+ borderRadius: '8px',
306
+ },
307
+ fileName: {
308
+ margin: '0 0 10px 0',
309
+ fontSize: '16px',
310
+ color: '#333',
311
+ },
312
+ button: {
313
+ padding: '10px 20px',
314
+ fontSize: '16px',
315
+ border: 'none',
316
+ borderRadius: '8px',
317
+ cursor: 'pointer',
318
+ transition: 'all 0.2s ease',
319
+ fontWeight: '500',
320
+ },
321
+ uploadButton: {
322
+ backgroundColor: '#4caf50',
323
+ color: 'white',
324
+ },
325
+ refreshButton: {
326
+ backgroundColor: '#2196f3',
327
+ color: 'white',
328
+ fontSize: '14px',
329
+ padding: '8px 16px',
330
+ },
331
+ deleteButton: {
332
+ backgroundColor: '#f44336',
333
+ color: 'white',
334
+ fontSize: '14px',
335
+ padding: '6px 12px',
336
+ },
337
+ buttonDisabled: {
338
+ opacity: 0.6,
339
+ cursor: 'not-allowed',
340
+ },
341
+ listHeader: {
342
+ display: 'flex',
343
+ justifyContent: 'space-between',
344
+ alignItems: 'center',
345
+ marginBottom: '20px',
346
+ },
347
+ filesList: {
348
+ display: 'flex',
349
+ flexDirection: 'column',
350
+ gap: '10px',
351
+ },
352
+ fileItem: {
353
+ display: 'flex',
354
+ justifyContent: 'space-between',
355
+ alignItems: 'center',
356
+ padding: '15px',
357
+ border: '1px solid #e0e0e0',
358
+ borderRadius: '8px',
359
+ backgroundColor: '#fafafa',
360
+ transition: 'background-color 0.2s ease',
361
+ },
362
+ fileInfo: {
363
+ flex: 1,
364
+ },
365
+ fileLink: {
366
+ color: '#1976d2',
367
+ textDecoration: 'none',
368
+ fontSize: '16px',
369
+ fontWeight: '500',
370
+ },
371
+ fileDate: {
372
+ margin: '5px 0 0 0',
373
+ fontSize: '13px',
374
+ color: '#888',
375
+ },
376
+ errorText: {
377
+ color: '#f44336',
378
+ marginTop: '10px',
379
+ fontSize: '14px',
380
+ },
381
+ loadingText: {
382
+ textAlign: 'center',
383
+ color: '#888',
384
+ fontSize: '16px',
385
+ },
386
+ emptyText: {
387
+ textAlign: 'center',
388
+ color: '#aaa',
389
+ fontSize: '16px',
390
+ padding: '30px',
391
+ },
392
+ };
393
+
394
+ export default FileManager;
395
+