@skalfa/skalfa-app-core 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/dist/api.util.d.ts +49 -0
- package/dist/api.util.js +125 -0
- package/dist/auth.util.d.ts +10 -0
- package/dist/auth.util.js +37 -0
- package/dist/cavity.util.d.ts +20 -0
- package/dist/cavity.util.js +124 -0
- package/dist/cn.util.d.ts +3 -0
- package/dist/cn.util.js +45 -0
- package/dist/commands/barrels.d.ts +1 -0
- package/dist/commands/barrels.js +22 -0
- package/dist/commands/blueprint.d.ts +3 -0
- package/dist/commands/blueprint.js +306 -0
- package/dist/commands/light.d.ts +1 -0
- package/dist/commands/light.js +16 -0
- package/dist/commands/logger.d.ts +10 -0
- package/dist/commands/logger.js +36 -0
- package/dist/commands/use-pdf.d.ts +1 -0
- package/dist/commands/use-pdf.js +17 -0
- package/dist/conversion.util.d.ts +10 -0
- package/dist/conversion.util.js +53 -0
- package/dist/encryption.util.d.ts +6 -0
- package/dist/encryption.util.js +56 -0
- package/dist/form.util.d.ts +87 -0
- package/dist/form.util.js +294 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +79 -0
- package/dist/langs/index.d.ts +1 -0
- package/dist/langs/index.js +1 -0
- package/dist/langs/validation.langs.d.ts +17 -0
- package/dist/langs/validation.langs.js +17 -0
- package/dist/registry/index.d.ts +19 -0
- package/dist/registry/index.js +10 -0
- package/dist/resource.util.d.ts +36 -0
- package/dist/resource.util.js +81 -0
- package/dist/shortcut.util.d.ts +12 -0
- package/dist/shortcut.util.js +22 -0
- package/dist/table.util.d.ts +51 -0
- package/dist/table.util.js +140 -0
- package/dist/validation.util.d.ts +18 -0
- package/dist/validation.util.js +150 -0
- package/package.json +47 -0
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useReducer, useEffect, useState } from "react";
|
|
3
|
+
import { registry } from "./registry";
|
|
4
|
+
import { validation } from "./validation.util";
|
|
5
|
+
import { api } from "./api.util";
|
|
6
|
+
const initialState = {
|
|
7
|
+
formRegisters: [],
|
|
8
|
+
formValues: [],
|
|
9
|
+
formErrors: [],
|
|
10
|
+
loading: false,
|
|
11
|
+
showConfirm: false,
|
|
12
|
+
};
|
|
13
|
+
// ==============================>
|
|
14
|
+
// ## Form state handler
|
|
15
|
+
// ==============================>
|
|
16
|
+
const formReducer = (state, action) => {
|
|
17
|
+
switch (action.type) {
|
|
18
|
+
// ==============================>
|
|
19
|
+
// ## Register handler
|
|
20
|
+
// ==============================>
|
|
21
|
+
case "SET_REGISTER": return {
|
|
22
|
+
...state,
|
|
23
|
+
formRegisters: [
|
|
24
|
+
...state.formRegisters.filter((reg) => reg.name !== action.payload.name),
|
|
25
|
+
action.payload,
|
|
26
|
+
],
|
|
27
|
+
};
|
|
28
|
+
// ==============================>
|
|
29
|
+
// ## Unregister single field — removes register, value, and error
|
|
30
|
+
// ==============================>
|
|
31
|
+
case "UNREGISTER": return {
|
|
32
|
+
...state,
|
|
33
|
+
formRegisters: state.formRegisters.filter((reg) => reg.name !== action.payload),
|
|
34
|
+
formValues: state.formValues.filter((val) => val.name !== action.payload),
|
|
35
|
+
formErrors: state.formErrors.filter((err) => err.name !== action.payload),
|
|
36
|
+
};
|
|
37
|
+
// ==============================>
|
|
38
|
+
// ## Unregister all fields matching prefix — for cluster group removal
|
|
39
|
+
// ==============================>
|
|
40
|
+
case "UNREGISTER_PREFIX": return {
|
|
41
|
+
...state,
|
|
42
|
+
formRegisters: state.formRegisters.filter((reg) => !reg.name.startsWith(action.payload)),
|
|
43
|
+
formValues: state.formValues.filter((val) => !val.name.startsWith(action.payload)),
|
|
44
|
+
formErrors: state.formErrors.filter((err) => !err.name.startsWith(action.payload)),
|
|
45
|
+
};
|
|
46
|
+
// ==============================>
|
|
47
|
+
// ## Multiple values handler
|
|
48
|
+
// ==============================>
|
|
49
|
+
case "SET_VALUES": return {
|
|
50
|
+
...state,
|
|
51
|
+
formValues: action.payload,
|
|
52
|
+
};
|
|
53
|
+
// ==============================>
|
|
54
|
+
// ## Single value handler
|
|
55
|
+
// ==============================>
|
|
56
|
+
case "SET_VALUE": return {
|
|
57
|
+
...state,
|
|
58
|
+
formValues: [
|
|
59
|
+
...state.formValues.filter((val) => val.name !== action.payload.name),
|
|
60
|
+
{ name: action.payload.name, value: action.payload.value },
|
|
61
|
+
],
|
|
62
|
+
};
|
|
63
|
+
// ==============================>
|
|
64
|
+
// ## Errors handler
|
|
65
|
+
// ==============================>
|
|
66
|
+
case "SET_ERRORS": return { ...state, formErrors: action.payload };
|
|
67
|
+
// ==============================>
|
|
68
|
+
// ## Loading handler
|
|
69
|
+
// ==============================>
|
|
70
|
+
case "SET_LOADING": return { ...state, loading: action.payload };
|
|
71
|
+
// ==============================>
|
|
72
|
+
// ## Confirm handler
|
|
73
|
+
// ==============================>
|
|
74
|
+
case "SET_CONFIRM": return { ...state, showConfirm: action.payload };
|
|
75
|
+
// ==============================>
|
|
76
|
+
// ## Reset handler
|
|
77
|
+
// ==============================>
|
|
78
|
+
case "RESET": return { ...initialState };
|
|
79
|
+
// ==============================>
|
|
80
|
+
// ## Return state
|
|
81
|
+
// ==============================>
|
|
82
|
+
default: return state;
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
// ==============================>
|
|
86
|
+
// ## Hook form
|
|
87
|
+
// ==============================>
|
|
88
|
+
export const useForm = (submitControl) => {
|
|
89
|
+
const isApiSubmit = !!submitControl?.path || !!submitControl?.url;
|
|
90
|
+
const isIdbSubmit = !!submitControl?.idb;
|
|
91
|
+
const [state, dispatch] = useReducer(formReducer, initialState);
|
|
92
|
+
const { payload, confirmation, onSuccess, onFailed } = submitControl;
|
|
93
|
+
// ==============================>
|
|
94
|
+
// ## Reset when first load
|
|
95
|
+
// ==============================>
|
|
96
|
+
useEffect(() => dispatch({ type: "RESET" }), [submitControl?.path, submitControl?.url, submitControl?.idb]);
|
|
97
|
+
// ==============================>
|
|
98
|
+
// ## Set value from changes
|
|
99
|
+
// ==============================>
|
|
100
|
+
const onChange = (name, value) => dispatch({ type: "SET_VALUE", payload: { name, value: value ?? "" } });
|
|
101
|
+
// ==============================>
|
|
102
|
+
// ## FormControl handler
|
|
103
|
+
// ==============================>
|
|
104
|
+
const formControl = (name) => ({
|
|
105
|
+
register: (_, regValidations) => dispatch({
|
|
106
|
+
type: "SET_REGISTER",
|
|
107
|
+
payload: { name, validations: regValidations },
|
|
108
|
+
}),
|
|
109
|
+
unregister: () => dispatch({ type: "UNREGISTER", payload: name }),
|
|
110
|
+
onChange: (e) => onChange(name, e),
|
|
111
|
+
value: state.formValues.find((val) => val.name === name)?.value || undefined,
|
|
112
|
+
invalid: state.formErrors.find((err) => err.name === name)?.error || undefined,
|
|
113
|
+
});
|
|
114
|
+
const getObjectValues = () => {
|
|
115
|
+
const registeredNames = new Set(state.formRegisters.map(r => r.name));
|
|
116
|
+
return state.formValues.reduce((acc, val) => {
|
|
117
|
+
if (registeredNames.has(val.name))
|
|
118
|
+
acc[val.name] = val.value;
|
|
119
|
+
return acc;
|
|
120
|
+
}, {});
|
|
121
|
+
};
|
|
122
|
+
const submitIdb = async () => {
|
|
123
|
+
const values = payload ? await payload(getObjectValues()) : getObjectValues();
|
|
124
|
+
const idb = registry.get("idb");
|
|
125
|
+
if (!idb) {
|
|
126
|
+
throw new Error("IndexedDB (IDB) extension is not installed or registered.");
|
|
127
|
+
}
|
|
128
|
+
const client = submitControl?.idb?.schema ? idb.useSchema(submitControl?.idb?.schema) : idb;
|
|
129
|
+
await client.put(submitControl?.idb?.store || "", values);
|
|
130
|
+
return { status: 200, data: values };
|
|
131
|
+
};
|
|
132
|
+
const submitApi = async () => {
|
|
133
|
+
const formData = new FormData();
|
|
134
|
+
const values = payload ? await payload(getObjectValues()) : getObjectValues();
|
|
135
|
+
Object.entries(values).forEach(([k, v]) => {
|
|
136
|
+
formData.append(k, v ?? "");
|
|
137
|
+
});
|
|
138
|
+
return api({
|
|
139
|
+
url: submitControl.url,
|
|
140
|
+
path: submitControl.path,
|
|
141
|
+
method: submitControl.method || "POST",
|
|
142
|
+
bearer: submitControl.bearer,
|
|
143
|
+
headers: submitControl.headers,
|
|
144
|
+
payload: formData,
|
|
145
|
+
});
|
|
146
|
+
};
|
|
147
|
+
// ==============================>
|
|
148
|
+
// ## Fetch to api
|
|
149
|
+
// ==============================>
|
|
150
|
+
const fetch = async () => {
|
|
151
|
+
dispatch({ type: "SET_LOADING", payload: true });
|
|
152
|
+
let execute;
|
|
153
|
+
if (isApiSubmit) {
|
|
154
|
+
execute = await submitApi();
|
|
155
|
+
}
|
|
156
|
+
else if (isIdbSubmit) {
|
|
157
|
+
execute = await submitIdb();
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
throw new Error("Invalid submitControl");
|
|
161
|
+
}
|
|
162
|
+
if (execute?.status === 200 || execute?.status === 201) {
|
|
163
|
+
// ==============================>
|
|
164
|
+
// ## When success
|
|
165
|
+
// ==============================>
|
|
166
|
+
dispatch({ type: "SET_LOADING", payload: false });
|
|
167
|
+
onSuccess?.(execute.data);
|
|
168
|
+
dispatch({ type: "RESET" });
|
|
169
|
+
}
|
|
170
|
+
else if (isApiSubmit && execute?.status === 422) {
|
|
171
|
+
// ==============================>
|
|
172
|
+
// ## When error invalid
|
|
173
|
+
// ==============================>
|
|
174
|
+
const errors = Object.keys(execute.data.errors).map((key) => ({
|
|
175
|
+
name: key,
|
|
176
|
+
error: execute.data.errors[key][0],
|
|
177
|
+
}));
|
|
178
|
+
onFailed?.(execute?.status || 500);
|
|
179
|
+
dispatch({ type: "SET_ERRORS", payload: errors });
|
|
180
|
+
dispatch({ type: "SET_LOADING", payload: false });
|
|
181
|
+
dispatch({ type: "SET_CONFIRM", payload: false });
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
// ==============================>
|
|
185
|
+
// ## When error server
|
|
186
|
+
// ==============================>
|
|
187
|
+
onFailed?.(execute?.status || 500);
|
|
188
|
+
dispatch({ type: "SET_CONFIRM", payload: false });
|
|
189
|
+
dispatch({ type: "SET_LOADING", payload: false });
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
// ==============================>
|
|
193
|
+
// ## Submit handler
|
|
194
|
+
// ==============================>
|
|
195
|
+
const submit = async (e) => {
|
|
196
|
+
e?.preventDefault();
|
|
197
|
+
dispatch({ type: "SET_ERRORS", payload: [] });
|
|
198
|
+
const newErrors = [];
|
|
199
|
+
// ==============================>
|
|
200
|
+
// ## Check register validation
|
|
201
|
+
// ==============================>
|
|
202
|
+
state.formRegisters.forEach((form) => {
|
|
203
|
+
const { valid, message } = validation.check({
|
|
204
|
+
value: state.formValues.find((val) => val.name === form.name)?.value,
|
|
205
|
+
rules: form.validations,
|
|
206
|
+
});
|
|
207
|
+
if (!valid) {
|
|
208
|
+
newErrors.push({ name: form.name, error: message });
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
if (newErrors.length) {
|
|
212
|
+
dispatch({ type: "SET_ERRORS", payload: newErrors });
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
// ==============================>
|
|
216
|
+
// ## Execute handler
|
|
217
|
+
// ==============================>
|
|
218
|
+
if (confirmation) {
|
|
219
|
+
dispatch({ type: "SET_CONFIRM", payload: true });
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
fetch();
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
// ==============================>
|
|
226
|
+
// ## Confirmation handler
|
|
227
|
+
// ==============================>
|
|
228
|
+
const onConfirm = () => fetch();
|
|
229
|
+
// ==============================>
|
|
230
|
+
// ## Set default value
|
|
231
|
+
// ==============================>
|
|
232
|
+
const setDefaultValues = (values) => {
|
|
233
|
+
const newValues = values ? Object.keys(values).map((keyName) => ({
|
|
234
|
+
name: keyName,
|
|
235
|
+
value: values[keyName],
|
|
236
|
+
})) : [];
|
|
237
|
+
dispatch({ type: "SET_VALUES", payload: newValues });
|
|
238
|
+
};
|
|
239
|
+
// ==============================>
|
|
240
|
+
// ## Return hook handler
|
|
241
|
+
// ==============================>
|
|
242
|
+
return {
|
|
243
|
+
submit,
|
|
244
|
+
formControl,
|
|
245
|
+
setDefaultValues,
|
|
246
|
+
values: state.formValues,
|
|
247
|
+
setValues: (values) => dispatch({ type: "SET_VALUES", payload: values || [] }),
|
|
248
|
+
errors: state.formErrors,
|
|
249
|
+
setErrors: (errors) => dispatch({ type: "SET_ERRORS", payload: errors }),
|
|
250
|
+
setRegister: (inputs) => dispatch({ type: "SET_REGISTER", payload: inputs }),
|
|
251
|
+
unregister: (name) => dispatch({ type: "UNREGISTER", payload: name }),
|
|
252
|
+
unregisterPrefix: (prefix) => dispatch({ type: "UNREGISTER_PREFIX", payload: prefix }),
|
|
253
|
+
loading: state.loading,
|
|
254
|
+
confirm: {
|
|
255
|
+
onConfirm,
|
|
256
|
+
show: state.showConfirm,
|
|
257
|
+
onClose: () => dispatch({ type: "SET_CONFIRM", payload: false }),
|
|
258
|
+
},
|
|
259
|
+
};
|
|
260
|
+
};
|
|
261
|
+
// ==============================>
|
|
262
|
+
// ## Generate random id
|
|
263
|
+
// ==============================>
|
|
264
|
+
export const useInputRandomId = () => {
|
|
265
|
+
const [randomId, setRandomId] = useState("");
|
|
266
|
+
useEffect(() => {
|
|
267
|
+
setRandomId(Math.random().toString(36).substring(7));
|
|
268
|
+
}, []);
|
|
269
|
+
return randomId;
|
|
270
|
+
};
|
|
271
|
+
// ==============================>
|
|
272
|
+
// ## Input handle
|
|
273
|
+
// ==============================>
|
|
274
|
+
export const useInputHandler = (name, value, validations, register, unregister, isFile) => {
|
|
275
|
+
const [inputValue, setInputValue] = useState("");
|
|
276
|
+
const [focus, setFocus] = useState(false);
|
|
277
|
+
const [idle, setIdle] = useState(true);
|
|
278
|
+
useEffect(() => {
|
|
279
|
+
name && register?.(name || "", validations);
|
|
280
|
+
return () => { name && unregister?.(name); };
|
|
281
|
+
}, [name, validations]);
|
|
282
|
+
useEffect(() => {
|
|
283
|
+
setInputValue(value && (!isFile || value instanceof File) ? value : "");
|
|
284
|
+
value && setIdle(false);
|
|
285
|
+
}, [value]);
|
|
286
|
+
return {
|
|
287
|
+
value: inputValue,
|
|
288
|
+
setValue: setInputValue,
|
|
289
|
+
idle,
|
|
290
|
+
setIdle,
|
|
291
|
+
focus,
|
|
292
|
+
setFocus
|
|
293
|
+
};
|
|
294
|
+
};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export * from "./api.util";
|
|
2
|
+
export * from "./auth.util";
|
|
3
|
+
export * from "./cavity.util";
|
|
4
|
+
export * from "./encryption.util";
|
|
5
|
+
export * from "./cn.util";
|
|
6
|
+
export * from "./form.util";
|
|
7
|
+
export * from "./resource.util";
|
|
8
|
+
export * from "./table.util";
|
|
9
|
+
export * from "./validation.util";
|
|
10
|
+
export * from "./conversion.util";
|
|
11
|
+
export * from "./shortcut.util";
|
|
12
|
+
export * from "./commands/logger";
|
|
13
|
+
export * from "./registry";
|
|
14
|
+
export declare const useResponsive: () => {
|
|
15
|
+
isXs: boolean;
|
|
16
|
+
isSm: boolean;
|
|
17
|
+
isMd: boolean;
|
|
18
|
+
isLg: boolean;
|
|
19
|
+
isXl: boolean;
|
|
20
|
+
isMobile: boolean;
|
|
21
|
+
isTablet: boolean;
|
|
22
|
+
isDesktop: boolean;
|
|
23
|
+
width: number;
|
|
24
|
+
height: number;
|
|
25
|
+
};
|
|
26
|
+
export declare function useKeyboardOpen(): boolean;
|
|
27
|
+
export declare const useLazySearch: (keyword: string) => string[];
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
// ==============================>
|
|
4
|
+
// ## Export all from core utils
|
|
5
|
+
// ==============================>
|
|
6
|
+
export * from "./api.util";
|
|
7
|
+
export * from "./auth.util";
|
|
8
|
+
export * from "./cavity.util";
|
|
9
|
+
export * from "./encryption.util";
|
|
10
|
+
export * from "./cn.util";
|
|
11
|
+
export * from "./form.util";
|
|
12
|
+
export * from "./resource.util";
|
|
13
|
+
export * from "./table.util";
|
|
14
|
+
export * from "./validation.util";
|
|
15
|
+
export * from "./conversion.util";
|
|
16
|
+
export * from "./shortcut.util";
|
|
17
|
+
export * from "./commands/logger";
|
|
18
|
+
export * from "./registry";
|
|
19
|
+
// ==============================>
|
|
20
|
+
// ## Detect device size
|
|
21
|
+
// ==============================>
|
|
22
|
+
export const useResponsive = () => {
|
|
23
|
+
const [windowSize, setWindowSize] = useState({ width: 0, height: 0 });
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
if (typeof window === "undefined")
|
|
26
|
+
return;
|
|
27
|
+
const handleResize = () => {
|
|
28
|
+
setWindowSize({ width: window.innerWidth, height: window.innerHeight });
|
|
29
|
+
};
|
|
30
|
+
handleResize();
|
|
31
|
+
window.addEventListener("resize", handleResize);
|
|
32
|
+
return () => window.removeEventListener("resize", handleResize);
|
|
33
|
+
}, []);
|
|
34
|
+
return {
|
|
35
|
+
isXs: windowSize.width < 640,
|
|
36
|
+
isSm: windowSize.width < 768,
|
|
37
|
+
isMd: windowSize.width < 1024,
|
|
38
|
+
isLg: windowSize.width < 1280,
|
|
39
|
+
isXl: windowSize.width >= 1280,
|
|
40
|
+
isMobile: windowSize.width < 768,
|
|
41
|
+
isTablet: windowSize.width >= 768 && windowSize.width < 1024,
|
|
42
|
+
isDesktop: windowSize.width >= 1024,
|
|
43
|
+
width: windowSize.width,
|
|
44
|
+
height: windowSize.height,
|
|
45
|
+
};
|
|
46
|
+
};
|
|
47
|
+
// ==============================>
|
|
48
|
+
// ## Detect keyboard open
|
|
49
|
+
// ==============================>
|
|
50
|
+
export function useKeyboardOpen() {
|
|
51
|
+
const [isKeyboardOpen, setIsKeyboardOpen] = useState(false);
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
const handleResize = () => {
|
|
54
|
+
if (window.visualViewport) {
|
|
55
|
+
const viewportHeight = window.visualViewport.height;
|
|
56
|
+
const windowHeight = window.innerHeight;
|
|
57
|
+
setIsKeyboardOpen(viewportHeight < windowHeight);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
window.visualViewport?.addEventListener("resize", handleResize);
|
|
61
|
+
return () => window.visualViewport?.removeEventListener("resize", handleResize);
|
|
62
|
+
}, []);
|
|
63
|
+
return isKeyboardOpen;
|
|
64
|
+
}
|
|
65
|
+
// ==============================>
|
|
66
|
+
// ## Search with typing reference
|
|
67
|
+
// ==============================>
|
|
68
|
+
export const useLazySearch = (keyword) => {
|
|
69
|
+
const [keywordSearch, setKeywordSearch] = useState("");
|
|
70
|
+
const [doSearch, setDoSearch] = useState(false);
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
if (keyword != undefined) {
|
|
73
|
+
const delaySearch = setTimeout(() => setDoSearch(!doSearch), 500);
|
|
74
|
+
return () => clearTimeout(delaySearch);
|
|
75
|
+
}
|
|
76
|
+
}, [keyword]);
|
|
77
|
+
useEffect(() => setKeywordSearch(keyword), [doSearch]);
|
|
78
|
+
return [keywordSearch];
|
|
79
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./validation.langs";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./validation.langs";
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export declare const validationLangs: {
|
|
2
|
+
required: string;
|
|
3
|
+
min: string;
|
|
4
|
+
max: string;
|
|
5
|
+
min_max: string;
|
|
6
|
+
phone: string;
|
|
7
|
+
url: string;
|
|
8
|
+
uppercase: string;
|
|
9
|
+
lowercase: string;
|
|
10
|
+
numeric: string;
|
|
11
|
+
email: string;
|
|
12
|
+
in: string;
|
|
13
|
+
not_in: string;
|
|
14
|
+
regex: string;
|
|
15
|
+
invalid_file_type: string;
|
|
16
|
+
max_file_size: string;
|
|
17
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export const validationLangs = {
|
|
2
|
+
required: "Please fill in this field!",
|
|
3
|
+
min: "Field must contain more than @min Character!",
|
|
4
|
+
max: "Field must be less than @max Character!",
|
|
5
|
+
min_max: "Field must be @min - @max Character!",
|
|
6
|
+
phone: "Please enter valid mobile number!",
|
|
7
|
+
url: "Please enter valid url!",
|
|
8
|
+
uppercase: "Field must be at least 1 uppercase!",
|
|
9
|
+
lowercase: "Field must be at least 1 lowercase!",
|
|
10
|
+
numeric: "Field must be at least 1 numeric!",
|
|
11
|
+
email: "Please enter valid email!",
|
|
12
|
+
in: "Field must be one of @keywords",
|
|
13
|
+
not_in: "Field can't one of @keywords",
|
|
14
|
+
regex: "Please enter valid format",
|
|
15
|
+
invalid_file_type: "Only allow extensions @extension",
|
|
16
|
+
max_file_size: "Max file size @maxFileSize Mb",
|
|
17
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface Registry {
|
|
2
|
+
idb?: any;
|
|
3
|
+
socket?: any;
|
|
4
|
+
ExportExcel?: any;
|
|
5
|
+
ImportExcel?: any;
|
|
6
|
+
[key: string]: any;
|
|
7
|
+
}
|
|
8
|
+
export type DBSchema = {
|
|
9
|
+
name: string;
|
|
10
|
+
version: number;
|
|
11
|
+
stores: Record<string, any>;
|
|
12
|
+
};
|
|
13
|
+
declare class ServiceRegistry {
|
|
14
|
+
private services;
|
|
15
|
+
register<K extends keyof Registry>(name: K, service: Registry[K]): void;
|
|
16
|
+
get<K extends keyof Registry>(name: K): Registry[K];
|
|
17
|
+
}
|
|
18
|
+
export declare const registry: ServiceRegistry;
|
|
19
|
+
export {};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { ApiType } from "./api.util";
|
|
2
|
+
export type ResourceParams = {
|
|
3
|
+
page?: number;
|
|
4
|
+
paginate?: number;
|
|
5
|
+
search?: string;
|
|
6
|
+
sort?: string[];
|
|
7
|
+
expand?: string[];
|
|
8
|
+
filter?: any[];
|
|
9
|
+
};
|
|
10
|
+
export type UseResourceApi = ApiType & {
|
|
11
|
+
method?: "GET";
|
|
12
|
+
};
|
|
13
|
+
export type UseResourceIdb = {
|
|
14
|
+
store: string;
|
|
15
|
+
schema?: any;
|
|
16
|
+
};
|
|
17
|
+
export type UseResourceProps = ({
|
|
18
|
+
path?: string;
|
|
19
|
+
url?: string;
|
|
20
|
+
} & UseResourceApi) | ({
|
|
21
|
+
idb: UseResourceIdb;
|
|
22
|
+
});
|
|
23
|
+
export declare function useResource(props: UseResourceProps & {
|
|
24
|
+
params?: ResourceParams;
|
|
25
|
+
}): {
|
|
26
|
+
loading: boolean;
|
|
27
|
+
data: any;
|
|
28
|
+
reset: () => Promise<"" | undefined>;
|
|
29
|
+
} | {
|
|
30
|
+
loading: boolean;
|
|
31
|
+
data: {
|
|
32
|
+
data: any[];
|
|
33
|
+
total_row: number;
|
|
34
|
+
} | null;
|
|
35
|
+
reset: () => Promise<void>;
|
|
36
|
+
};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
import { useGetApi } from "./api.util";
|
|
4
|
+
import { registry } from "./registry";
|
|
5
|
+
export function useResource(props) {
|
|
6
|
+
const isApi = "path" in props || "url" in props;
|
|
7
|
+
const apiResult = useGetApi(isApi ? props : {}, !isApi);
|
|
8
|
+
// =====================
|
|
9
|
+
// IDB MODE
|
|
10
|
+
// =====================
|
|
11
|
+
const [loading, setLoading] = useState(false);
|
|
12
|
+
const [data, setData] = useState(null);
|
|
13
|
+
const idbParams = props.params || {};
|
|
14
|
+
const fetchIdb = async () => {
|
|
15
|
+
if (!("idb" in props))
|
|
16
|
+
return;
|
|
17
|
+
setLoading(true);
|
|
18
|
+
try {
|
|
19
|
+
const idb = registry.get("idb");
|
|
20
|
+
if (!idb) {
|
|
21
|
+
throw new Error("IndexedDB (IDB) extension is not installed or registered.");
|
|
22
|
+
}
|
|
23
|
+
const idbClient = props.idb.schema
|
|
24
|
+
? idb.useSchema(props.idb.schema)
|
|
25
|
+
: idb;
|
|
26
|
+
let q = await idbClient.query(props.idb.store);
|
|
27
|
+
if (idbParams.search) {
|
|
28
|
+
const keyword = idbParams.search.toLowerCase();
|
|
29
|
+
q = q.where((row) => Object.values(row).some((v) => String(v).toLowerCase().includes(keyword)));
|
|
30
|
+
}
|
|
31
|
+
if (Array.isArray(idbParams.filter)) {
|
|
32
|
+
for (const f of idbParams.filter) {
|
|
33
|
+
if (f?.field && f?.value !== undefined) {
|
|
34
|
+
q = q.where((row) => row[f.field] === f.value);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (Array.isArray(idbParams.sort) && idbParams.sort.length) {
|
|
39
|
+
q = q.usingIndex(idbParams.sort[0]?.split(" ")?.at(0) || "created_at").order(idbParams.sort[0]?.split(" ")?.at(1) == "asc" ? "asc" : "desc");
|
|
40
|
+
}
|
|
41
|
+
if (idbParams.paginate) {
|
|
42
|
+
q = q.paginate(idbParams.page || 0, idbParams.paginate);
|
|
43
|
+
}
|
|
44
|
+
const [rows, total] = await Promise.all([
|
|
45
|
+
q.get(),
|
|
46
|
+
q.count(),
|
|
47
|
+
]);
|
|
48
|
+
// const rows = await q.get()
|
|
49
|
+
setData({ data: rows, total_row: total });
|
|
50
|
+
}
|
|
51
|
+
finally {
|
|
52
|
+
setLoading(false);
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
if (!isApi && "idb" in props)
|
|
57
|
+
fetchIdb();
|
|
58
|
+
}, [
|
|
59
|
+
isApi,
|
|
60
|
+
idbParams.search,
|
|
61
|
+
JSON.stringify(idbParams.filter),
|
|
62
|
+
JSON.stringify(idbParams.sort),
|
|
63
|
+
idbParams.paginate,
|
|
64
|
+
idbParams.page,
|
|
65
|
+
]);
|
|
66
|
+
// =====================
|
|
67
|
+
// Unified return
|
|
68
|
+
// =====================
|
|
69
|
+
if (isApi) {
|
|
70
|
+
return {
|
|
71
|
+
loading: apiResult.loading,
|
|
72
|
+
data: apiResult.data,
|
|
73
|
+
reset: apiResult.reset,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
loading,
|
|
78
|
+
data: data,
|
|
79
|
+
reset: fetchIdb,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export type ShortcutHandler = (e: KeyboardEvent) => void;
|
|
2
|
+
export type ShortcutType = {
|
|
3
|
+
key: string;
|
|
4
|
+
description?: string;
|
|
5
|
+
handler: ShortcutHandler;
|
|
6
|
+
};
|
|
7
|
+
export declare const shortcut: {
|
|
8
|
+
register: (key: string, handler: ShortcutHandler, description?: string) => void;
|
|
9
|
+
unregister: (key: string) => boolean;
|
|
10
|
+
list: () => ShortcutType[];
|
|
11
|
+
init: () => void;
|
|
12
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
const handlers = new Map();
|
|
2
|
+
export const shortcut = {
|
|
3
|
+
register: (key, handler, description) => {
|
|
4
|
+
handlers.set(key, { key, handler, description });
|
|
5
|
+
},
|
|
6
|
+
unregister: (key) => handlers.delete(key),
|
|
7
|
+
list: () => Array.from(handlers.values()),
|
|
8
|
+
init: () => {
|
|
9
|
+
window.addEventListener("keydown", (e) => {
|
|
10
|
+
const target = e.target;
|
|
11
|
+
if (target?.tagName === "INPUT" || target?.tagName === "TEXTAREA" || target?.isContentEditable)
|
|
12
|
+
return;
|
|
13
|
+
const combo = [e.ctrlKey && "ctrl", e.shiftKey && "shift", e.altKey && "alt", e.key.toLowerCase()].filter(Boolean).join("+");
|
|
14
|
+
const meta = handlers.get(combo);
|
|
15
|
+
if (meta) {
|
|
16
|
+
e.preventDefault();
|
|
17
|
+
e.stopPropagation();
|
|
18
|
+
meta.handler(e);
|
|
19
|
+
}
|
|
20
|
+
}, true);
|
|
21
|
+
}
|
|
22
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { ApiFilterType, ApiParamsType } from "./api.util";
|
|
2
|
+
import { ResourceParams, UseResourceProps } from "./resource.util";
|
|
3
|
+
export type TableStateType = {
|
|
4
|
+
params?: ApiParamsType;
|
|
5
|
+
data?: Record<string, any>[];
|
|
6
|
+
selected?: Record<string, any> | null;
|
|
7
|
+
checks?: (string | number)[] | null;
|
|
8
|
+
focus?: number | null;
|
|
9
|
+
};
|
|
10
|
+
export type FetchControlType = {
|
|
11
|
+
path?: string;
|
|
12
|
+
url?: string;
|
|
13
|
+
headers?: Record<string, any>;
|
|
14
|
+
params?: ApiParamsType;
|
|
15
|
+
includeParams?: object;
|
|
16
|
+
bearer?: string;
|
|
17
|
+
};
|
|
18
|
+
export declare const useTable: (fetchControl: UseResourceProps & {
|
|
19
|
+
params?: ResourceParams;
|
|
20
|
+
}, id: string | undefined, title: string | undefined, urlParam: boolean | {
|
|
21
|
+
compressed?: boolean;
|
|
22
|
+
}) => {
|
|
23
|
+
tableKey: string;
|
|
24
|
+
data: any;
|
|
25
|
+
reset: (() => Promise<"" | undefined>) | (() => Promise<void>);
|
|
26
|
+
loading: boolean;
|
|
27
|
+
params: ApiParamsType | undefined;
|
|
28
|
+
setParam: <K extends keyof ApiParamsType>(key: K, value: ApiParamsType[K]) => void;
|
|
29
|
+
focus: number | null | undefined;
|
|
30
|
+
setFocus: (focus: number | null) => void;
|
|
31
|
+
selected: Record<string, any> | null | undefined;
|
|
32
|
+
setSelected: (selected: Record<string, any> | null) => void;
|
|
33
|
+
checks: (string | number)[] | null | undefined;
|
|
34
|
+
setChecks: (checks: (string | number)[] | null) => void;
|
|
35
|
+
tableControl: {
|
|
36
|
+
loading: boolean;
|
|
37
|
+
sortBy: string[] | undefined;
|
|
38
|
+
onChangeSortBy: (e: string[]) => void;
|
|
39
|
+
search: string | undefined;
|
|
40
|
+
onChangeSearch: (e: string) => void;
|
|
41
|
+
filter: ApiFilterType[] | undefined;
|
|
42
|
+
onChangeFilter: (e: ApiFilterType[]) => void;
|
|
43
|
+
onRefresh: () => Promise<void> | Promise<"" | undefined>;
|
|
44
|
+
pagination: {
|
|
45
|
+
totalRow: any;
|
|
46
|
+
page: number;
|
|
47
|
+
paginate: number;
|
|
48
|
+
onChange: (_: number, paginate: number, page: number) => void;
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
};
|