@xeplr/ui-utils 1.0.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.
- package/LICENSE +21 -0
- package/package.json +16 -0
- package/src/data/countries.json +786 -0
- package/src/data/states.json +296 -0
- package/src/fileUpload/designs/FileUploadSample.jsx +77 -0
- package/src/fileUpload/designs/fileUpload.css +161 -0
- package/src/fileUpload/designs/index.js +1 -0
- package/src/fileUpload/pages.jsx +9 -0
- package/src/fileUpload/uploadFile.js +76 -0
- package/src/fileUpload/useFileUploadController.js +163 -0
- package/src/fileUpload/validateDesign.js +49 -0
- package/src/index.js +23 -0
- package/src/snackbar/snackbar.js +72 -0
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { useState, useRef } from 'react';
|
|
2
|
+
import { uploadFile } from './uploadFile.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @param {object} options
|
|
6
|
+
* @param {string[]} [options.allowedTypes] - MIME types: ['image/*', 'application/pdf']
|
|
7
|
+
* @param {number} [options.maxSize] - Max file size in bytes
|
|
8
|
+
* @param {number} [options.maxFiles=1] - Max number of files
|
|
9
|
+
* @param {string} [options.url] - Upload URL override
|
|
10
|
+
* @param {string} [options.fieldName='file'] - Form field name
|
|
11
|
+
* @param {object} [options.extraData] - Additional form data
|
|
12
|
+
* @param {function} [options.onSuccess] - Called with server response
|
|
13
|
+
* @param {function} [options.onError] - Called with error
|
|
14
|
+
*/
|
|
15
|
+
export function useFileUploadController(options = {}) {
|
|
16
|
+
var [files, setFiles] = useState([]);
|
|
17
|
+
var [uploading, setUploading] = useState(false);
|
|
18
|
+
var [progress, setProgress] = useState(0);
|
|
19
|
+
var [error, setError] = useState('');
|
|
20
|
+
var [result, setResult] = useState(null);
|
|
21
|
+
var [dragOver, setDragOver] = useState(false);
|
|
22
|
+
var inputRef = useRef(null);
|
|
23
|
+
|
|
24
|
+
var allowedTypes = options.allowedTypes || ['*'];
|
|
25
|
+
var maxSize = options.maxSize || 5 * 1024 * 1024;
|
|
26
|
+
var maxFiles = options.maxFiles || 1;
|
|
27
|
+
|
|
28
|
+
function isTypeAllowed(mimetype) {
|
|
29
|
+
for (var i = 0; i < allowedTypes.length; i++) {
|
|
30
|
+
var allowed = allowedTypes[i];
|
|
31
|
+
if (allowed === '*') return true;
|
|
32
|
+
if (allowed.endsWith('/*')) {
|
|
33
|
+
var category = allowed.split('/')[0];
|
|
34
|
+
if (mimetype.startsWith(category + '/')) return true;
|
|
35
|
+
}
|
|
36
|
+
if (allowed === mimetype) return true;
|
|
37
|
+
}
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function validate(fileList) {
|
|
42
|
+
var validated = [];
|
|
43
|
+
for (var i = 0; i < fileList.length; i++) {
|
|
44
|
+
var file = fileList[i];
|
|
45
|
+
if (!isTypeAllowed(file.type)) {
|
|
46
|
+
return { error: 'File type not allowed: ' + file.name };
|
|
47
|
+
}
|
|
48
|
+
if (file.size > maxSize) {
|
|
49
|
+
return { error: 'File too large: ' + file.name };
|
|
50
|
+
}
|
|
51
|
+
validated.push(file);
|
|
52
|
+
}
|
|
53
|
+
if (validated.length > maxFiles) {
|
|
54
|
+
return { error: 'Too many files. Maximum: ' + maxFiles };
|
|
55
|
+
}
|
|
56
|
+
return { files: validated };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function handleFiles(fileList) {
|
|
60
|
+
setError('');
|
|
61
|
+
setResult(null);
|
|
62
|
+
var validation = validate(fileList);
|
|
63
|
+
if (validation.error) {
|
|
64
|
+
setError(validation.error);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
setFiles(validation.files);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function handleChange(e) {
|
|
71
|
+
handleFiles(Array.from(e.target.files));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function handleDragOver(e) {
|
|
75
|
+
e.preventDefault();
|
|
76
|
+
setDragOver(true);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function handleDragLeave(e) {
|
|
80
|
+
e.preventDefault();
|
|
81
|
+
setDragOver(false);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function handleDrop(e) {
|
|
85
|
+
e.preventDefault();
|
|
86
|
+
setDragOver(false);
|
|
87
|
+
handleFiles(Array.from(e.dataTransfer.files));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function openFilePicker() {
|
|
91
|
+
if (inputRef.current) {
|
|
92
|
+
inputRef.current.click();
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function removeFile(index) {
|
|
97
|
+
setFiles(function(prev) {
|
|
98
|
+
var next = prev.slice();
|
|
99
|
+
next.splice(index, 1);
|
|
100
|
+
return next;
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function clear() {
|
|
105
|
+
setFiles([]);
|
|
106
|
+
setError('');
|
|
107
|
+
setResult(null);
|
|
108
|
+
setProgress(0);
|
|
109
|
+
if (inputRef.current) {
|
|
110
|
+
inputRef.current.value = '';
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function handleUpload() {
|
|
115
|
+
if (files.length === 0) return;
|
|
116
|
+
setError('');
|
|
117
|
+
setUploading(true);
|
|
118
|
+
setProgress(0);
|
|
119
|
+
try {
|
|
120
|
+
var response = await uploadFile(files, {
|
|
121
|
+
url: options.url,
|
|
122
|
+
fieldName: options.fieldName,
|
|
123
|
+
extraData: options.extraData,
|
|
124
|
+
onProgress: function(percent) {
|
|
125
|
+
setProgress(percent);
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
setResult(response);
|
|
129
|
+
if (options.onSuccess) options.onSuccess(response);
|
|
130
|
+
} catch (err) {
|
|
131
|
+
setError(err.message);
|
|
132
|
+
if (options.onError) options.onError(err);
|
|
133
|
+
} finally {
|
|
134
|
+
setUploading(false);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
// State
|
|
140
|
+
files,
|
|
141
|
+
uploading,
|
|
142
|
+
progress,
|
|
143
|
+
error,
|
|
144
|
+
result,
|
|
145
|
+
dragOver,
|
|
146
|
+
inputRef,
|
|
147
|
+
|
|
148
|
+
// Actions
|
|
149
|
+
handleChange,
|
|
150
|
+
handleDragOver,
|
|
151
|
+
handleDragLeave,
|
|
152
|
+
handleDrop,
|
|
153
|
+
handleUpload,
|
|
154
|
+
openFilePicker,
|
|
155
|
+
removeFile,
|
|
156
|
+
clear,
|
|
157
|
+
|
|
158
|
+
// Config (for view to read)
|
|
159
|
+
allowedTypes,
|
|
160
|
+
maxSize,
|
|
161
|
+
maxFiles
|
|
162
|
+
};
|
|
163
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Validates that required elements exist in the rendered design.
|
|
5
|
+
* Throws a visible error if any required element is missing.
|
|
6
|
+
*
|
|
7
|
+
* @param {string} componentName - Name of the page (for error messages)
|
|
8
|
+
* @param {Array<{id?: string, role?: string, selector?: string, label: string}>} requiredElements
|
|
9
|
+
*/
|
|
10
|
+
export function useDesignValidator(componentName, requiredElements) {
|
|
11
|
+
var containerRef = useRef(null);
|
|
12
|
+
|
|
13
|
+
useEffect(function() {
|
|
14
|
+
if (!containerRef.current) return;
|
|
15
|
+
|
|
16
|
+
var missing = [];
|
|
17
|
+
for (var i = 0; i < requiredElements.length; i++) {
|
|
18
|
+
var rule = requiredElements[i];
|
|
19
|
+
var found = false;
|
|
20
|
+
|
|
21
|
+
if (rule.id) {
|
|
22
|
+
found = !!containerRef.current.querySelector('#' + rule.id);
|
|
23
|
+
} else if (rule.role) {
|
|
24
|
+
found = !!containerRef.current.querySelector('[role="' + rule.role + '"]');
|
|
25
|
+
} else if (rule.selector) {
|
|
26
|
+
found = !!containerRef.current.querySelector(rule.selector);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (!found) {
|
|
30
|
+
missing.push(rule.label + (rule.id ? ' (id="' + rule.id + '")' : '') + (rule.selector ? ' (' + rule.selector + ')' : ''));
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (missing.length > 0) {
|
|
35
|
+
throw new Error(
|
|
36
|
+
'[xeplr-ui-utils] ' + componentName + ' design is missing required elements:\n' +
|
|
37
|
+
missing.map(function(m) { return ' - ' + m; }).join('\n')
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
}, []);
|
|
41
|
+
|
|
42
|
+
return containerRef;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export var FILE_UPLOAD_RULES = [
|
|
46
|
+
{ selector: 'input[type="file"]', label: 'File input' },
|
|
47
|
+
{ selector: '.xeplr-upload-dropzone, [role="dropzone"]', label: 'Drop zone area' },
|
|
48
|
+
{ selector: 'button', label: 'Upload/action button' }
|
|
49
|
+
];
|
package/src/index.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// File Upload — model
|
|
2
|
+
export { configureUpload, uploadFile } from './fileUpload/uploadFile.js';
|
|
3
|
+
|
|
4
|
+
// File Upload — controller
|
|
5
|
+
export { useFileUploadController } from './fileUpload/useFileUploadController.js';
|
|
6
|
+
|
|
7
|
+
// File Upload — design validation
|
|
8
|
+
export { useDesignValidator, FILE_UPLOAD_RULES } from './fileUpload/validateDesign.js';
|
|
9
|
+
|
|
10
|
+
// File Upload — sample design
|
|
11
|
+
export { FileUploadSample } from './fileUpload/designs/index.js';
|
|
12
|
+
|
|
13
|
+
// File Upload — ready-made page (controller + sample design wired)
|
|
14
|
+
export { FileUploadPage } from './fileUpload/pages.jsx';
|
|
15
|
+
|
|
16
|
+
// Snackbar
|
|
17
|
+
export { raiseSnackbar } from './snackbar/snackbar.js';
|
|
18
|
+
|
|
19
|
+
// Static data
|
|
20
|
+
export { default as COUNTRIES } from './data/countries.json';
|
|
21
|
+
import _statesData from './data/states.json';
|
|
22
|
+
export var STATES = _statesData.STATES;
|
|
23
|
+
export var STATES_IN = _statesData.STATES_IN;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
var _container = null;
|
|
2
|
+
var _queue = [];
|
|
3
|
+
var _timer = null;
|
|
4
|
+
|
|
5
|
+
var DESIGNS = {
|
|
6
|
+
success: { bg: '#059669', color: '#fff' },
|
|
7
|
+
error: { bg: '#dc2626', color: '#fff' },
|
|
8
|
+
default: { bg: '#2563eb', color: '#fff' }
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
var DEFAULT_DURATION = 3000;
|
|
12
|
+
|
|
13
|
+
function _ensureContainer() {
|
|
14
|
+
if (_container) return _container;
|
|
15
|
+
|
|
16
|
+
_container = document.createElement('div');
|
|
17
|
+
_container.id = 'xeplr-snackbar-container';
|
|
18
|
+
_container.style.cssText = 'position:fixed;bottom:24px;right:24px;z-index:99999;display:flex;flex-direction:column-reverse;gap:8px;pointer-events:none;';
|
|
19
|
+
document.body.appendChild(_container);
|
|
20
|
+
return _container;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function _createEl(message, design, duration) {
|
|
24
|
+
var colors = DESIGNS[design] || DESIGNS.default;
|
|
25
|
+
|
|
26
|
+
var el = document.createElement('div');
|
|
27
|
+
el.style.cssText = 'padding:12px 20px;border-radius:6px;font:14px/1.4 system-ui,sans-serif;max-width:380px;word-break:break-word;pointer-events:auto;opacity:0;transform:translateY(8px);transition:opacity 0.2s,transform 0.2s;cursor:pointer;'
|
|
28
|
+
+ 'background:' + colors.bg + ';color:' + colors.color + ';';
|
|
29
|
+
el.textContent = message;
|
|
30
|
+
|
|
31
|
+
el.onclick = function() { _dismiss(el); };
|
|
32
|
+
|
|
33
|
+
// Animate in
|
|
34
|
+
requestAnimationFrame(function() {
|
|
35
|
+
el.style.opacity = '1';
|
|
36
|
+
el.style.transform = 'translateY(0)';
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// Auto dismiss
|
|
40
|
+
el._timeout = setTimeout(function() { _dismiss(el); }, duration);
|
|
41
|
+
|
|
42
|
+
return el;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function _dismiss(el) {
|
|
46
|
+
if (el._dismissed) return;
|
|
47
|
+
el._dismissed = true;
|
|
48
|
+
clearTimeout(el._timeout);
|
|
49
|
+
el.style.opacity = '0';
|
|
50
|
+
el.style.transform = 'translateY(8px)';
|
|
51
|
+
setTimeout(function() {
|
|
52
|
+
if (el.parentNode) el.parentNode.removeChild(el);
|
|
53
|
+
}, 200);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Show a snackbar notification.
|
|
58
|
+
*
|
|
59
|
+
* @param {string} message - Text to display
|
|
60
|
+
* @param {object} [options]
|
|
61
|
+
* @param {string} [options.design='default'] - 'success' | 'error' | 'default'
|
|
62
|
+
* @param {number} [options.duration=3000] - Auto-dismiss in ms
|
|
63
|
+
*/
|
|
64
|
+
export function raiseSnackbar(message, options) {
|
|
65
|
+
options = options || {};
|
|
66
|
+
var design = options.design || 'default';
|
|
67
|
+
var duration = options.duration || DEFAULT_DURATION;
|
|
68
|
+
|
|
69
|
+
var container = _ensureContainer();
|
|
70
|
+
var el = _createEl(message, design, duration);
|
|
71
|
+
container.appendChild(el);
|
|
72
|
+
}
|