@skalfa/skalfa-app 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.
Files changed (112) hide show
  1. package/.env.example +44 -0
  2. package/README.md +28 -0
  3. package/app/auth/edit/page.tsx +65 -0
  4. package/app/auth/login/page.tsx +63 -0
  5. package/app/auth/me/page.tsx +58 -0
  6. package/app/auth/register/page.tsx +69 -0
  7. package/app/auth/verify/page.tsx +53 -0
  8. package/app/dashboard/layout.tsx +47 -0
  9. package/app/dashboard/page.tsx +9 -0
  10. package/app/dashboard/user/page.tsx +77 -0
  11. package/app/index.ts +14 -0
  12. package/app/layout.tsx +38 -0
  13. package/app/page.tsx +13 -0
  14. package/barrels.json +6 -0
  15. package/blueprints/starter.blueprint.json +103 -0
  16. package/components/base.components/accordion/Accordion.component.tsx +82 -0
  17. package/components/base.components/breadcrumb/Breadcrumb.component.tsx +80 -0
  18. package/components/base.components/button/Button.component.tsx +91 -0
  19. package/components/base.components/button/IconButton.component.tsx +88 -0
  20. package/components/base.components/button/button.decorate.ts +82 -0
  21. package/components/base.components/card/AlertCard.component.tsx +69 -0
  22. package/components/base.components/card/Card.component.tsx +25 -0
  23. package/components/base.components/card/DashboardCard.component.tsx +44 -0
  24. package/components/base.components/card/GalleryCard.component.tsx +50 -0
  25. package/components/base.components/card/ProductCard.component.tsx +65 -0
  26. package/components/base.components/card/ProfileCard.component.tsx +71 -0
  27. package/components/base.components/carousel/Carousel.component.tsx +113 -0
  28. package/components/base.components/chip/Chip.component.tsx +39 -0
  29. package/components/base.components/document/DocumentViewer.component.tsx +164 -0
  30. package/components/base.components/document/ExportExcel.component.tsx +340 -0
  31. package/components/base.components/document/ImportExcel.component.tsx +315 -0
  32. package/components/base.components/document/PrintTable.component.tsx +204 -0
  33. package/components/base.components/document/RenderPDF.component.tsx +416 -0
  34. package/components/base.components/index.ts +85 -0
  35. package/components/base.components/input/Checkbox.component.tsx +109 -0
  36. package/components/base.components/input/Input.component.tsx +332 -0
  37. package/components/base.components/input/InputCheckbox.component.tsx +174 -0
  38. package/components/base.components/input/InputCurrency.component.tsx +163 -0
  39. package/components/base.components/input/InputDate.component.tsx +352 -0
  40. package/components/base.components/input/InputDatetime.component.tsx +260 -0
  41. package/components/base.components/input/InputDocument.component.tsx +352 -0
  42. package/components/base.components/input/InputImage.component.tsx +533 -0
  43. package/components/base.components/input/InputMap.component.tsx +318 -0
  44. package/components/base.components/input/InputNumber.component.tsx +192 -0
  45. package/components/base.components/input/InputOtp.component.tsx +169 -0
  46. package/components/base.components/input/InputPassword.component.tsx +236 -0
  47. package/components/base.components/input/InputRadio.component.tsx +175 -0
  48. package/components/base.components/input/InputTime.component.tsx +276 -0
  49. package/components/base.components/input/InputValues.component.tsx +68 -0
  50. package/components/base.components/input/Radio.component.tsx +102 -0
  51. package/components/base.components/input/Select.component.tsx +541 -0
  52. package/components/base.components/modal/BottomSheet.component.tsx +246 -0
  53. package/components/base.components/modal/FloatingPage.component.tsx +104 -0
  54. package/components/base.components/modal/Modal.component.tsx +96 -0
  55. package/components/base.components/modal/ModalConfirm.component.tsx +218 -0
  56. package/components/base.components/modal/Toast.component.tsx +126 -0
  57. package/components/base.components/nav/Bottombar.component.tsx +116 -0
  58. package/components/base.components/nav/Footer.component.tsx +144 -0
  59. package/components/base.components/nav/Headbar.component.tsx +104 -0
  60. package/components/base.components/nav/Navbar.component.tsx +100 -0
  61. package/components/base.components/nav/Sidebar.component.tsx +301 -0
  62. package/components/base.components/nav/Tabbar.component.tsx +60 -0
  63. package/components/base.components/nav/Wizard.component.tsx +73 -0
  64. package/components/base.components/supervision/FormSupervision.component.tsx +434 -0
  65. package/components/base.components/supervision/TableSupervision.component.tsx +697 -0
  66. package/components/base.components/table/ControlBar.component.tsx +497 -0
  67. package/components/base.components/table/FilterComponent.tsx +518 -0
  68. package/components/base.components/table/Pagination.component.tsx +159 -0
  69. package/components/base.components/table/Table.component.tsx +469 -0
  70. package/components/base.components/typography/TypographyArticle.component.tsx +26 -0
  71. package/components/base.components/typography/TypographyColumn.component.tsx +20 -0
  72. package/components/base.components/typography/TypographyContent.component.tsx +20 -0
  73. package/components/base.components/typography/TypographyTips.component.tsx +20 -0
  74. package/components/base.components/wrap/Draggable.component.tsx +303 -0
  75. package/components/base.components/wrap/IDBProvider.tsx +12 -0
  76. package/components/base.components/wrap/Image.component.tsx +10 -0
  77. package/components/base.components/wrap/OutsideClick.component.tsx +48 -0
  78. package/components/base.components/wrap/ScrollContainer.component.tsx +104 -0
  79. package/components/base.components/wrap/ShortcutProvider.tsx +57 -0
  80. package/components/base.components/wrap/Swipe.component.tsx +93 -0
  81. package/components/construct.components/example.tsx +1 -0
  82. package/components/construct.components/index.ts +5 -0
  83. package/components/index.ts +3 -0
  84. package/components/structure.components/example.tsx +1 -0
  85. package/components/structure.components/index.ts +5 -0
  86. package/contexts/AppProvider.tsx +12 -0
  87. package/contexts/Auth.context.tsx +64 -0
  88. package/contexts/Toggle.context.tsx +44 -0
  89. package/contexts/index.ts +7 -0
  90. package/eslint.config.mjs +34 -0
  91. package/langs/index.ts +1 -0
  92. package/langs/validation.langs.ts +17 -0
  93. package/next.config.ts +17 -0
  94. package/package.json +43 -0
  95. package/postcss.config.mjs +12 -0
  96. package/public/204.svg +19 -0
  97. package/public/500.svg +39 -0
  98. package/public/images/avatar.jpg +0 -0
  99. package/public/images/example.png +0 -0
  100. package/schema/idb/app.schema.ts +9 -0
  101. package/schema/index.ts +5 -0
  102. package/styles/globals.css +231 -0
  103. package/styles/tailwind.safelist +69 -0
  104. package/tailwind.config.ts +10 -0
  105. package/tsconfig.json +35 -0
  106. package/utils/commands/barrels.ts +28 -0
  107. package/utils/commands/blueprint.ts +421 -0
  108. package/utils/commands/light.ts +21 -0
  109. package/utils/commands/logger.ts +42 -0
  110. package/utils/commands/stubs/table-blueprint.stub +13 -0
  111. package/utils/commands/use-pdf.ts +29 -0
  112. package/utils/index.ts +3 -0
@@ -0,0 +1,109 @@
1
+ "use client"
2
+
3
+ import { ReactNode, useEffect, useState } from "react";
4
+ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
5
+ import { faCheck } from "@fortawesome/free-solid-svg-icons";
6
+ import { cn, pcn, useInputRandomId } from "@utils";
7
+
8
+
9
+
10
+ type CT = "label" | "checked" | "error" | "base";
11
+
12
+ export type CheckboxProps = {
13
+ name : string;
14
+ label ?: string | ReactNode;
15
+
16
+ value ?: string;
17
+ disabled ?: boolean;
18
+ checked ?: boolean;
19
+ invalid ?: string;
20
+
21
+ onChange ?: () => void;
22
+
23
+ /** Use custom class with: "label::", "checked::", "error::". */
24
+ className ?: string;
25
+ };
26
+
27
+
28
+
29
+ export function CheckboxComponent({
30
+ name,
31
+ label,
32
+
33
+ value,
34
+ disabled = false,
35
+ checked = false,
36
+ invalid,
37
+
38
+ onChange,
39
+
40
+ className = "",
41
+ }: CheckboxProps) {
42
+
43
+
44
+ // =========================>
45
+ // ## Initial
46
+ // =========================>
47
+ const randomId = useInputRandomId()
48
+ const [invalidMessage, setInvalidMessage] = useState("");
49
+
50
+
51
+ // =========================>
52
+ // ## Invalid handler
53
+ // =========================>
54
+ useEffect(() => {
55
+ setInvalidMessage(invalid || "");
56
+ }, [invalid]);
57
+
58
+
59
+ return (
60
+ <div className={`flex flex-col gap-1`}>
61
+ <input
62
+ type="checkbox"
63
+ className="hidden"
64
+ id={randomId}
65
+ name={name}
66
+ onChange={onChange}
67
+ defaultChecked={checked}
68
+ value={value}
69
+ disabled={disabled}
70
+ />
71
+
72
+ <label
73
+ htmlFor={randomId}
74
+ className={cn(
75
+ "flex gap-2 items-center cursor-pointer active:scale-x-[102%]",
76
+ disabled && "pointer-events-none opacity-60"
77
+ )}
78
+ >
79
+ <div>
80
+ <div
81
+ className={cn(
82
+ `flex justify-center items-center rounded-md border w-6 h-6 transition-colors border-light-foreground text-light-foreground`,
83
+ checked && "border-light-primary bg-primary !text-background",
84
+ checked && pcn<CT>(className, "checked"),
85
+ pcn<CT>(className, "base"),
86
+ )}
87
+ >
88
+ {checked && <FontAwesomeIcon icon={faCheck} className="text-sm" />}
89
+ </div>
90
+ </div>
91
+ <span
92
+ className={cn(
93
+ "whitespace-nowrap",
94
+ checked && "font-semibold",
95
+ pcn<CT>(className, "label"),
96
+ checked && pcn<CT>(className, "label", "checked"),
97
+ disabled && pcn<CT>(className, "label", "disabled"),
98
+ )}
99
+ >
100
+ {label}
101
+ </span>
102
+ </label>
103
+
104
+ {invalidMessage && (
105
+ <small className={cn("input-error-message", pcn<CT>(className, "error"))}>{invalidMessage}</small>
106
+ )}
107
+ </div>
108
+ );
109
+ }
@@ -0,0 +1,332 @@
1
+ "use client"
2
+
3
+ import { InputHTMLAttributes, ReactNode, Ref, useEffect, useState } from "react";
4
+ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
5
+ import { cn, pcn, useInputHandler, useInputRandomId, useValidation, validation, ValidationRules } from "@utils";
6
+ import { InputValues } from "./InputValues.component";
7
+
8
+
9
+
10
+ type CT = "label" | "tip" | "error" | "base" | "icon" | "suggest" | "suggest-item";
11
+
12
+ export interface InputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, "onChange"> {
13
+ label ?: string;
14
+ tip ?: string | ReactNode;
15
+ leftIcon ?: any;
16
+ rightIcon ?: any;
17
+
18
+ value ?: any;
19
+ invalid ?: string;
20
+ suggestions ?: string[];
21
+
22
+ validations ?: ValidationRules;
23
+ onlyAlphabet ?: boolean;
24
+ uppercase ?: boolean;
25
+ lowercase ?: boolean;
26
+ multiple ?: boolean;
27
+
28
+ onChange ?: (value: any) => any;
29
+ register ?: (name: string, validations?: ValidationRules) => void;
30
+ unregister ?: (name: string) => void;
31
+
32
+ ref ?: Ref<HTMLInputElement>,
33
+
34
+ /** Use custom class with: "label::", "tip::", "error::", "icon::", "suggest::", "suggest-item::". */
35
+ className ?: string;
36
+ }
37
+
38
+
39
+
40
+ export function InputComponent({
41
+ label,
42
+ tip,
43
+ leftIcon,
44
+ rightIcon,
45
+ className = "",
46
+
47
+ value,
48
+ invalid,
49
+ suggestions,
50
+
51
+ validations,
52
+ onlyAlphabet,
53
+ uppercase,
54
+ lowercase,
55
+ multiple,
56
+
57
+ register,
58
+ unregister,
59
+ onChange,
60
+
61
+ ref,
62
+ ...props
63
+ }: InputProps) {
64
+
65
+
66
+ const [activeSuggestion, setActiveSuggestion] = useState(0);
67
+ const [showSuggestions, setShowSuggestions] = useState(false);
68
+ const [dataSuggestions, setDataSuggestions] = useState<string[] | undefined>([]);
69
+ const [filteredSuggestions, setFilteredSuggestions] = useState<string[] | undefined>([]);
70
+
71
+
72
+ // =========================>
73
+ // ## Initial
74
+ // =========================>
75
+ const inputHandler = useInputHandler(props.name, value, validations, register, unregister, props.type == "file")
76
+ const randomId = useInputRandomId()
77
+
78
+
79
+ // =========================>
80
+ // ## Invalid handler
81
+ // =========================>
82
+ const [invalidMessage] = useValidation(inputHandler.value, validations, invalid, inputHandler.idle);
83
+
84
+
85
+ // =========================>
86
+ // ## Change value handler
87
+ // =========================>
88
+ useEffect(() => {
89
+ if (inputHandler.value && typeof inputHandler.value === "string") {
90
+ let newVal = onlyAlphabet ? inputHandler.value.replace(/[^A-Za-z ]+/g, "") : inputHandler.value;
91
+
92
+ if (uppercase) newVal = newVal.toUpperCase();
93
+ if (lowercase) newVal = newVal.toLowerCase();
94
+
95
+ if (validations && validation.hasRules(validations, "max")) newVal = newVal.slice(0, parseInt(validation.getRules(validations, "max") || "0"));
96
+
97
+ inputHandler.setValue(newVal);
98
+ }
99
+ }, [inputHandler.value, onlyAlphabet, uppercase, lowercase, validations]);
100
+
101
+
102
+ // =========================>
103
+ // ## suggestions handler
104
+ // =========================>
105
+ useEffect(() => {
106
+ setDataSuggestions(suggestions);
107
+ }, [suggestions]);
108
+
109
+ const filterSuggestion = (e: any) => {
110
+ if (dataSuggestions?.length) {
111
+ let filteredSuggestion = [];
112
+
113
+ if (e.target.value) {
114
+ filteredSuggestion = dataSuggestions
115
+ .filter((suggestion) => suggestion.toLowerCase().indexOf(e.target.value.toLowerCase()) > -1)
116
+ .slice(0, 10);
117
+ } else {
118
+ filteredSuggestion = dataSuggestions.slice(0, 10);
119
+ }
120
+
121
+ setActiveSuggestion(-1);
122
+ setFilteredSuggestions(filteredSuggestion);
123
+ setShowSuggestions(true);
124
+ }
125
+ };
126
+
127
+
128
+ const onKeyDownSuggestion = (e: any) => {
129
+ if (dataSuggestions?.length) {
130
+ if (e.keyCode === 13) {
131
+ const resultValue = filteredSuggestions?.at(activeSuggestion);
132
+ setActiveSuggestion(-1);
133
+ setFilteredSuggestions([]);
134
+ setShowSuggestions(false);
135
+ inputHandler.setValue(resultValue ? resultValue : inputHandler.value);
136
+ if (onChange) {
137
+ onChange(resultValue ? resultValue : inputHandler.value);
138
+ }
139
+ e.preventDefault();
140
+ } else if (e.keyCode === 38) {
141
+ if (activeSuggestion === 0) {
142
+ return;
143
+ }
144
+
145
+ setActiveSuggestion(activeSuggestion - 1);
146
+ } else if (e.keyCode === 40) {
147
+ if (activeSuggestion + 1 >= (filteredSuggestions?.length || 0)) {
148
+ return;
149
+ }
150
+
151
+ setActiveSuggestion(activeSuggestion + 1);
152
+ }
153
+ }
154
+ };
155
+
156
+ return (
157
+ <>
158
+ <div className="relative flex flex-col gap-y-0.5">
159
+ <label
160
+ htmlFor={randomId}
161
+ className={cn(
162
+ "input-label",
163
+ pcn<CT>(className, "label"),
164
+ props.disabled && "opacity-50",
165
+ props.disabled && pcn<CT>(className, "label", "disabled"),
166
+ inputHandler.focus && "text-primary",
167
+ inputHandler.focus && pcn<CT>(className, "label", "focus"),
168
+ !!invalidMessage && "text-danger",
169
+ !!invalidMessage && pcn<CT>(className, "label", "focus"),
170
+ )}
171
+ >
172
+ {label}
173
+ {validations && validation.hasRules(validations, "required") && <span className="text-danger ml-1">*</span>}
174
+ </label>
175
+
176
+ {tip && (
177
+ <small
178
+ className={cn(
179
+ "input-tip",
180
+ pcn<CT>(className, "tip"),
181
+ props.disabled && "opacity-60",
182
+ props.disabled && pcn<CT>(className, "tip", "disabled"),
183
+ )}
184
+ >{tip}</small>
185
+ )}
186
+
187
+ <div className="relative">
188
+ <input
189
+ {...props}
190
+ ref={ref}
191
+ id={randomId}
192
+ placeholder={!multiple || (multiple && !inputHandler.value?.length) ? props.placeholder : ""}
193
+ className={cn(
194
+ "input",
195
+ props.type == "file" && "input-file",
196
+ leftIcon && "pl-12",
197
+ rightIcon && "pr-12",
198
+ pcn<CT>(className, "base"),
199
+ !!invalidMessage && "input-error",
200
+ !!invalidMessage && pcn<CT>(className, "base", "error"),
201
+ )}
202
+ value={!multiple ? inputHandler.value: undefined}
203
+ onChange={(e) => {
204
+ if(!multiple) {
205
+ inputHandler.setValue(e.target.value);
206
+ inputHandler.setIdle(false);
207
+ onChange?.(props.type == "file" ? e.target?.files && e.target?.files[0] : e.target.value);
208
+ dataSuggestions?.length && filterSuggestion(e);
209
+ }
210
+ }}
211
+ onFocus={(e) => {
212
+ props.onFocus?.(e);
213
+ inputHandler.setFocus(true);
214
+ dataSuggestions?.length && filterSuggestion(e);
215
+ }}
216
+ onBlur={(e) => {
217
+ props.onBlur?.(e);
218
+ setTimeout(() => inputHandler.setFocus(false), 100);
219
+ }}
220
+ onKeyDown={(e) => {
221
+ dataSuggestions?.length && onKeyDownSuggestion(e);
222
+
223
+ if (multiple && e.key === "Enter" || e.key === ",") {
224
+ e.preventDefault();
225
+ const currentValue = e.currentTarget.value.trim();
226
+ if (!currentValue) return;
227
+
228
+ const currentValues = Array.isArray(inputHandler.value) ? [...inputHandler.value] : [];
229
+ if (!currentValues.includes(currentValue)) {
230
+ const newValues = [...currentValues, currentValue];
231
+ onChange?.(newValues);
232
+ inputHandler.setValue(newValues);
233
+ e.currentTarget.value = "";
234
+ }
235
+ }
236
+ }}
237
+ autoComplete={props.autoComplete || dataSuggestions?.length ? "off" : ""}
238
+ />
239
+
240
+
241
+ {(multiple) && (
242
+ <InputValues
243
+ value={inputHandler.value || []}
244
+ isFocus={inputHandler.focus}
245
+ onFocus={() => setTimeout(() => inputHandler.setFocus(true), 110)}
246
+ onDelete={(_, index) => {
247
+ const values = Array().concat(inputHandler.value);
248
+ const newValues = values.filter((_, val) => val != index);
249
+
250
+ inputHandler.setValue(newValues);
251
+ onChange?.(newValues);
252
+ }}
253
+ className={`${!inputHandler.focus && (leftIcon ? "ml-[2.5rem]" : "ml-[1rem]")}`}
254
+ style={{ maxWidth: `calc(100% - ${leftIcon ? "5.2rem" : "2rem"})` }}
255
+ />
256
+ )}
257
+
258
+ {leftIcon && (
259
+ <FontAwesomeIcon
260
+ className={cn(
261
+ "left-4 input-icon",
262
+ pcn<CT>(className, "icon"),
263
+ props.disabled && "opacity-60",
264
+ props.disabled && pcn<CT>(className, "icon", "disabled"),
265
+ inputHandler.focus && "text-primary",
266
+ inputHandler.focus && pcn<CT>(className, "icon", "focus"),
267
+ )}
268
+ icon={leftIcon}
269
+ />
270
+ )}
271
+
272
+ {rightIcon && (
273
+ <FontAwesomeIcon
274
+ className={cn(
275
+ "right-4 input-icon",
276
+ pcn<CT>(className, "icon"),
277
+ props.disabled && "opacity-60",
278
+ props.disabled && pcn<CT>(className, "icon", "disabled"),
279
+ inputHandler.focus && "text-primary",
280
+ inputHandler.focus && pcn<CT>(className, "icon", "focus"),
281
+ )}
282
+ icon={rightIcon}
283
+ />
284
+ )}
285
+ </div>
286
+
287
+ {!!dataSuggestions?.length && showSuggestions && !!filteredSuggestions?.length && (
288
+ <div>
289
+ <ul
290
+ className={cn(
291
+ "input-suggest-container",
292
+ pcn<CT>(className, "suggest"),
293
+ inputHandler.focus ? "opacity-100 scale-y-100 -translate-y-0" : "opacity-0 scale-y-0 -translate-y-1/2",
294
+ )}
295
+ >
296
+ {filteredSuggestions.map((suggestion, key) => {
297
+ return (
298
+ <li
299
+ className={cn(
300
+ "input-suggest",
301
+ pcn<CT>(className, "suggest-item"),
302
+ inputHandler.value == suggestion && "bg-light-primary text-primary",
303
+ inputHandler.value == suggestion && pcn<CT>(className, "suggest-item", "active"),
304
+ )}
305
+ key={suggestion}
306
+ onMouseDown={() => {
307
+ setTimeout(() => inputHandler.setFocus(true), 110);
308
+ }}
309
+ onMouseUp={() => {
310
+ setActiveSuggestion(key);
311
+ setFilteredSuggestions([]);
312
+ setShowSuggestions(false);
313
+ inputHandler.setValue(filteredSuggestions[key] || inputHandler.value);
314
+ onChange?.(filteredSuggestions[key] || inputHandler.value);
315
+ setTimeout(() => inputHandler.setFocus(false), 120);
316
+ }}
317
+ >
318
+ {suggestion}
319
+ </li>
320
+ );
321
+ })}
322
+ </ul>
323
+ </div>
324
+ )}
325
+
326
+ {invalidMessage && (
327
+ <small className={cn("input-error-message", pcn<CT>(className, "error"))}>{invalidMessage}</small>
328
+ )}
329
+ </div>
330
+ </>
331
+ );
332
+ }
@@ -0,0 +1,174 @@
1
+ "use client"
2
+
3
+ import { ReactNode, useEffect, useState } from "react";
4
+ import { api, ApiType, cn, pcn, useInputHandler, useValidation, validation, ValidationRules } from "@utils";
5
+ import { CheckboxComponent } from "@components";
6
+
7
+
8
+
9
+ type CT = "label" | "tip" | "error" | "input" | "icon";
10
+
11
+ export interface InputCheckboxOptionProps {
12
+ value: string | number;
13
+ label: string;
14
+ };
15
+
16
+ export interface InputCheckboxProps {
17
+ name : string;
18
+ label ?: string;
19
+ tip ?: string | ReactNode;
20
+ vertical ?: boolean;
21
+
22
+ value ?: string[] | number[];
23
+ disabled ?: boolean;
24
+ invalid ?: string;
25
+
26
+ options ?: InputCheckboxOptionProps[];
27
+ serverOptionControl ?: ApiType;
28
+ customOptions ?: any;
29
+ validations ?: ValidationRules;
30
+
31
+ onChange ?: (value: string[] | number[]) => any;
32
+ register ?: (name: string, validations?: ValidationRules) => void;
33
+ unregister ?: (name: string) => void;
34
+
35
+ /** Use custom class with: "label::", "tip::", "error::", "icon::". */
36
+ className ?: string;
37
+
38
+ /** Use custom class with: "label::", "checked::", "error::". */
39
+ classNameCheckbox ?: string;
40
+ };
41
+
42
+
43
+
44
+ export function InputCheckboxComponent({
45
+ name,
46
+ label,
47
+ tip,
48
+ vertical,
49
+ className="",
50
+ classNameCheckbox="",
51
+
52
+ value,
53
+ disabled,
54
+ invalid,
55
+
56
+ options,
57
+ serverOptionControl,
58
+ customOptions,
59
+ validations,
60
+
61
+ register,
62
+ unregister,
63
+ onChange,
64
+ }: InputCheckboxProps) {
65
+
66
+ const [dataOptions, setDataOptions] = useState<InputCheckboxOptionProps[]>([]);
67
+ const [loading, setLoading] = useState(false);
68
+
69
+
70
+ // =========================>
71
+ // ## initial
72
+ // =========================>
73
+ const inputHandler = useInputHandler(name, value, validations, register, unregister, false)
74
+
75
+
76
+ // =========================>
77
+ // ## Invalid handler
78
+ // =========================>
79
+ const [invalidMessage] = useValidation(inputHandler.value, validations, invalid, inputHandler.idle);
80
+
81
+
82
+ // =========================>
83
+ // ## fetch option
84
+ // =========================>
85
+ useEffect(() => {
86
+ const fetchOptions = async () => {
87
+ setLoading(true);
88
+ const mutateOptions = await api(serverOptionControl || {});
89
+ if (mutateOptions?.status == 200) {
90
+ customOptions ? setDataOptions([customOptions, ...mutateOptions.data]) : setDataOptions(mutateOptions.data);
91
+ setLoading(false);
92
+ }
93
+ };
94
+
95
+ if (serverOptionControl?.path || serverOptionControl?.url) {
96
+ fetchOptions();
97
+ } else {
98
+ !options && setDataOptions([]);
99
+ }
100
+ }, [serverOptionControl?.path, serverOptionControl?.url]);
101
+
102
+
103
+ return (
104
+ <>
105
+ <div className="w-full relative flex flex-col gap-y-0.5">
106
+ <label
107
+ className={cn(
108
+ "input-label",
109
+ pcn<CT>(className, "label"),
110
+ disabled && "opacity-50",
111
+ disabled && pcn<CT>(className, "label", "disabled"),
112
+ invalidMessage && "text-danger",
113
+ invalidMessage && pcn<CT>(className, "label", "focus"),
114
+ )}
115
+ >
116
+ {label}
117
+ {validations && validation.hasRules(validations, "required") && <span className="text-danger ml-1">*</span>}
118
+ </label>
119
+
120
+ {tip && (
121
+ <small
122
+ className={cn(
123
+ "input-tip",
124
+ pcn<CT>(className, "tip"),
125
+ disabled && "opacity-60",
126
+ disabled && pcn<CT>(className, "tip", "disabled"),
127
+ )}
128
+ >{tip}</small>
129
+ )}
130
+
131
+ <div
132
+ className={cn(
133
+ `input overflow-auto input-scroll w-full flex flex-nowrap gap-y-2 gap-4`,
134
+ vertical && "flex-col flex-wrap",
135
+ pcn<CT>(className, "input"),
136
+ invalidMessage && "input-error",
137
+ invalidMessage && pcn<CT>(className, "input", "error"),
138
+ )}
139
+ >
140
+ {loading && (vertical ? [1, 2, 3, 4, 5, 6, 7, 8, 9] : [1, 2, 3]).map((_, key) => {
141
+ return <div key={key} className="w-1/3 h-6 rounded-lg"></div>;
142
+ })}
143
+
144
+ {(options || dataOptions) && (options || dataOptions)?.map((option, key) => {
145
+ const checked = Array().concat(inputHandler.value).find((val) => val == option.value);
146
+
147
+ return (
148
+ <CheckboxComponent
149
+ key={key}
150
+ label={option.label}
151
+ name={`option[${option.value}]#${name}`}
152
+ checked={!!checked}
153
+ disabled={disabled}
154
+ className={classNameCheckbox}
155
+ onChange={() => {
156
+ const newVal = (Array.isArray(inputHandler.value) ? inputHandler.value : [])
157
+ .filter((val) => val !== option.value)
158
+ .concat(checked ? [] : [option.value]);
159
+
160
+ inputHandler.setValue(newVal);
161
+ onChange?.(newVal);
162
+ }}
163
+ />
164
+ );
165
+ })}
166
+ </div>
167
+
168
+ {invalidMessage && (
169
+ <small className={cn("input-error-message", pcn<CT>(className, "error"))}>{invalidMessage}</small>
170
+ )}
171
+ </div>
172
+ </>
173
+ );
174
+ }