@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,541 @@
1
+ "use client"
2
+
3
+ import { ReactNode, useEffect, useState } from "react";
4
+ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
5
+ import { faCheck, faChevronDown, faTimes,} from "@fortawesome/free-solid-svg-icons";
6
+ import { api, ApiType, cavity, cn, pcn, registry, useInputHandler, useInputRandomId, useLazySearch, useValidation, validation, ValidationRules,} from "@utils";
7
+
8
+
9
+
10
+ type CT = "label" | "tip" | "error" | "input" | "icon" | "suggest" | "suggest-item";
11
+
12
+ export interface SelectOptionProps {
13
+ label : string | ReactNode;
14
+ value : string | number;
15
+ searchable ?: string[];
16
+ customLabel ?: ReactNode;
17
+ };
18
+
19
+ export interface SelectProps {
20
+ name : string;
21
+ label ?: string;
22
+ placeholder ?: string;
23
+ tip ?: string | ReactNode;
24
+ leftIcon ?: any;
25
+ rightIcon ?: any;
26
+
27
+ value ?: string | number | (string | number)[];
28
+ invalid ?: string;
29
+ disabled ?: boolean;
30
+ validations ?: ValidationRules;
31
+ multiple ?: boolean;
32
+ autoFocus ?: boolean;
33
+ clearable ?: boolean;
34
+
35
+ options ?: SelectOptionProps[];
36
+ searchable ?: boolean;
37
+ serverOptionControl ?: ApiType & { cacheName?: string | false };
38
+ idbOptionControl ?: { store: string, labelKey: string, valueKey: string };
39
+ serverSearchable ?: boolean;
40
+ includedOptions ?: SelectOptionProps[];
41
+ exceptOptions ?: (string | number)[];
42
+ tempOptions ?: SelectOptionProps[];
43
+ newOption ?: SelectOptionProps;
44
+ maxShowOption ?: number;
45
+
46
+ onChange ?: (value: string | number | (string | number)[], data?: any) => any;
47
+ register ?: (name: string, validations?: ValidationRules) => void;
48
+ unregister ?: (name: string) => void;
49
+ onFocus ?: () => void;
50
+ onBlur ?: () => void;
51
+
52
+ /** Use custom class with: "label::", "tip::", "error::", "icon::", "suggest::", "suggest-item::". */
53
+ className ?: string;
54
+ }
55
+
56
+
57
+
58
+ export function SelectComponent({
59
+ name,
60
+ label,
61
+ placeholder,
62
+ tip,
63
+ leftIcon,
64
+ rightIcon,
65
+
66
+ value,
67
+ invalid,
68
+ disabled,
69
+ validations,
70
+ multiple,
71
+ autoFocus,
72
+ clearable,
73
+
74
+ options = [],
75
+ searchable,
76
+ serverOptionControl,
77
+ idbOptionControl,
78
+ serverSearchable,
79
+ includedOptions = [],
80
+ exceptOptions = [],
81
+ tempOptions,
82
+ newOption,
83
+ maxShowOption = 10,
84
+
85
+ register,
86
+ unregister,
87
+ onChange,
88
+ onFocus,
89
+ onBlur,
90
+
91
+ className = "",
92
+ }: SelectProps) {
93
+ const [inputShowValue, setInputShowValue] = useState<string | ReactNode>("");
94
+ const [keydown, setKeydown] = useState(false);
95
+ const [useTemp, setUseTemp] = useState(true);
96
+ const [dataOptions, setDataOptions] = useState<SelectOptionProps[]>([]);
97
+ const [filteredOptions, setFilteredOptions] = useState<SelectOptionProps[]>([]);
98
+ const [loadingOption, setLoadingOption] = useState(false);
99
+ const [activeOption, setActiveOption] = useState(0);
100
+ const [showOption, setShowOption] = useState(false);
101
+ const [keyword, setKeyword] = useState("");
102
+ const [keywordSearch] = useLazySearch(keyword);
103
+
104
+
105
+ // =========================>
106
+ // ## Initial
107
+ // =========================>
108
+ const inputHandler = useInputHandler(name, value, validations, register, unregister, false)
109
+ const randomId = useInputRandomId()
110
+
111
+
112
+ // =========================>
113
+ // ## Invalid handler
114
+ // =========================>
115
+ const [invalidMessage] = useValidation(inputHandler.value, validations, invalid, inputHandler.idle);
116
+
117
+
118
+ // =========================>
119
+ // ## change value handler
120
+ // =========================>
121
+ useEffect(() => {
122
+ if (value) {
123
+ inputHandler.setValue(value);
124
+ Array.isArray(dataOptions) && setInputShowValue((newOption ? [newOption, ...dataOptions] : dataOptions)?.find((option) => option.value == value)?.label || "");
125
+ inputHandler.setIdle(false);
126
+ } else {
127
+ inputHandler.setValue("");
128
+ setInputShowValue("");
129
+ }
130
+ }, [value, dataOptions]);
131
+
132
+
133
+ // =========================>
134
+ // ## options handler
135
+ // =========================>
136
+ useEffect(() => {
137
+ options?.length && setDataOptions([...options, ...includedOptions].filter((op: SelectOptionProps) => !exceptOptions?.includes(op.value)));
138
+ }, [options]);
139
+
140
+
141
+ const filterOption = (e: any) => {
142
+ if (dataOptions?.length) {
143
+ let newFilteredOptions: SelectOptionProps[] = [];
144
+
145
+ if (searchable && !serverSearchable) {
146
+ if (e.target.value) {
147
+ newFilteredOptions = dataOptions.filter((Option) => (Option.label as string)?.toLowerCase().indexOf(e.target.value.toLowerCase()) > -1).slice(0, maxShowOption);
148
+ } else {
149
+ newFilteredOptions = dataOptions.slice(0, maxShowOption);
150
+ }
151
+ } else {
152
+ newFilteredOptions = dataOptions;
153
+ }
154
+
155
+ setActiveOption(-1);
156
+ setFilteredOptions(newFilteredOptions);
157
+ setShowOption(true);
158
+ }
159
+ };
160
+
161
+
162
+ const onKeyDownOption = (e: any) => {
163
+ if (dataOptions?.length) {
164
+ if (e.keyCode === 13) {
165
+ const resultValue = filteredOptions?.at(activeOption);
166
+ setActiveOption(-1);
167
+ setFilteredOptions([]);
168
+ setShowOption(false);
169
+ if (!multiple) {
170
+ setInputShowValue(resultValue?.label || inputShowValue);
171
+ inputHandler.setValue(resultValue?.value || inputShowValue);
172
+ serverSearchable && setKeyword((resultValue?.label as string) || keyword);
173
+ } else {
174
+ if (resultValue?.value) {
175
+ searchable ? setInputShowValue(resultValue.label) : searchable && setInputShowValue("");
176
+ serverSearchable && setKeyword(resultValue.label as string);
177
+
178
+ const values: string[] = Array.isArray(inputHandler.value) ? Array().concat(inputHandler.value)?.filter((val: string | number) => val != resultValue?.value) : [];
179
+
180
+ if (values.find((val) => val == resultValue?.value)) {
181
+ inputHandler.setValue(values);
182
+ } else {
183
+ inputHandler.setValue([...Array().concat(values), resultValue.value]);
184
+ }
185
+ }
186
+ }
187
+ e.preventDefault();
188
+ } else if (e.keyCode === 38) {
189
+ if (activeOption === 0) return;
190
+ setActiveOption(activeOption - 1);
191
+ } else if (e.keyCode === 40) {
192
+ if (activeOption + 1 >= (filteredOptions?.length || 0)) return;
193
+ setActiveOption(activeOption + 1);
194
+ }
195
+ }
196
+ };
197
+
198
+ const fetchOptions = async () => {
199
+ setLoadingOption(true);
200
+
201
+ const serverControl = {
202
+ ...serverOptionControl,
203
+ params: serverSearchable ? { search: keywordSearch, ...(serverOptionControl?.params || {}) } : (serverOptionControl?.params || {}),
204
+ headers: { "X-Option": 1 }
205
+ };
206
+
207
+ const getCacheOptions = await cavity.get(serverOptionControl?.cacheName || `option_${serverOptionControl?.path}`)
208
+ const cacheOptions = (getCacheOptions?.data || []) as SelectOptionProps[];
209
+
210
+ if (cacheOptions?.length) {
211
+ setDataOptions(
212
+ [...cacheOptions, ...includedOptions].filter(
213
+ (op: SelectOptionProps) => !exceptOptions?.includes(op.value)
214
+ )
215
+ );
216
+ setLoadingOption(false);
217
+ } else {
218
+ const mutateOptions = await api(serverControl || {});
219
+ setDataOptions(
220
+ [...(mutateOptions?.data?.data || []), ...(includedOptions || [])].filter(
221
+ (op: SelectOptionProps) => !exceptOptions?.includes(op.value)
222
+ )
223
+ );
224
+ setShowOption(true);
225
+
226
+ if(serverOptionControl?.cacheName != false) {
227
+ cavity.set({
228
+ key: serverOptionControl?.cacheName || `option_${serverOptionControl?.path}`,
229
+ data: mutateOptions?.data,
230
+ expired: 5,
231
+ });
232
+ }
233
+ setLoadingOption(false);
234
+ }
235
+ };
236
+
237
+ const fetchIdbOptions = async () => {
238
+ setLoadingOption(true);
239
+
240
+ if (idbOptionControl?.store) {
241
+ const idb = registry.get("idb");
242
+ if (!idb) {
243
+ throw new Error("IndexedDB (IDB) extension is not installed.");
244
+ }
245
+ const getIdbOptions = await (await idb.query(idbOptionControl?.store)).get()
246
+
247
+ const rows = getIdbOptions.map((row: Record<string,any>) => {
248
+ const value = row[idbOptionControl.valueKey] || row["id"];
249
+ const label = row[idbOptionControl.labelKey] || row["id"];
250
+
251
+ return {
252
+ label,
253
+ value,
254
+ ...row,
255
+ }
256
+ })
257
+
258
+ setDataOptions(rows);
259
+ setLoadingOption(false);
260
+ }
261
+ };
262
+
263
+ useEffect(() => {
264
+ if (!serverSearchable) {
265
+ if (serverOptionControl?.path || serverOptionControl?.url) {
266
+ fetchOptions();
267
+ } else if (idbOptionControl?.store) {
268
+ fetchIdbOptions();
269
+ } else {
270
+ !options && setDataOptions([]);
271
+ }
272
+ }
273
+
274
+ }, [serverOptionControl?.path, serverOptionControl?.url]);
275
+
276
+ useEffect(() => {
277
+ if (serverSearchable) {
278
+ if (serverOptionControl?.path || serverOptionControl?.url) {
279
+ fetchOptions();
280
+ } else {
281
+ !options && setDataOptions([]);
282
+ }
283
+ }
284
+ }, [keywordSearch, serverOptionControl?.path, serverOptionControl?.url]);
285
+
286
+ return (
287
+ <>
288
+ <div className="relative flex flex-col gap-y-0.5">
289
+ <label
290
+ htmlFor={randomId}
291
+ className={cn(
292
+ "input-label",
293
+ pcn<CT>(className, "label"),
294
+ disabled && "opacity-50",
295
+ disabled && pcn<CT>(className, "label", "disabled"),
296
+ inputHandler.focus && "text-primary",
297
+ inputHandler.focus && pcn<CT>(className, "label", "focus"),
298
+ invalidMessage && "text-danger",
299
+ invalidMessage && pcn<CT>(className, "label", "focus")
300
+ )}
301
+ >
302
+ {label}
303
+ {validations && validation.hasRules(validations, "required") && <span className="text-danger ml-1">*</span>}
304
+ </label>
305
+
306
+ {tip && (
307
+ <small
308
+ className={cn(
309
+ "input-tip",
310
+ pcn<CT>(className, "tip"),
311
+ disabled && "opacity-60",
312
+ disabled && pcn<CT>(className, "tip", "disabled")
313
+ )}
314
+ >{tip}</small>
315
+ )}
316
+
317
+ <div className="relative">
318
+ <input
319
+ type="hidden"
320
+ value={!multiple ? String(inputHandler.value) : Array().concat(inputHandler.value).map((val) => String(val))}
321
+ name={name}
322
+ />
323
+ <input
324
+ type="text"
325
+ readOnly={!searchable}
326
+ id={randomId}
327
+ placeholder={!inputHandler.value || (Array.isArray(inputHandler.value) && !inputHandler.value.length) ? placeholder : ""}
328
+ disabled={disabled}
329
+ className={cn(
330
+ "input cursor-pointer",
331
+ leftIcon && "pl-12",
332
+ rightIcon && "pr-12",
333
+ pcn<CT>(className, "input"),
334
+ invalidMessage && "input-error",
335
+ invalidMessage && pcn<CT>(className, "input", "error")
336
+ )}
337
+ value={(useTemp && tempOptions ? tempOptions.at(0)?.label : serverSearchable ? keyword : inputShowValue) as string}
338
+ onChange={(e) => {
339
+ setUseTemp(false);
340
+ searchable && setInputShowValue(e.target.value);
341
+ serverSearchable && setKeyword(e.target.value);
342
+ inputHandler.setIdle(false);
343
+ dataOptions?.length && filterOption(e);
344
+ }}
345
+ onFocus={(e) => {
346
+ setUseTemp(false);
347
+ inputHandler.setFocus(true);
348
+ onFocus?.();
349
+ dataOptions?.length && filterOption(e);
350
+ searchable && e.target.select();
351
+ }}
352
+ onBlur={(e) => {
353
+ setUseTemp(false);
354
+ const value = e.target.value;
355
+ const valueOption = dataOptions?.find((option) => (option.label as string)?.toLowerCase() == value?.toLowerCase());
356
+
357
+ if (!keydown) {
358
+ if (!multiple) {
359
+ setTimeout(() => {
360
+ if (valueOption?.value) {
361
+ setInputShowValue(valueOption.label);
362
+ inputHandler.setValue(valueOption.value);
363
+ serverSearchable && setKeyword(valueOption.label as string);
364
+ onChange?.(valueOption.value, valueOption);
365
+ } else {
366
+ setInputShowValue("");
367
+ serverSearchable && setKeyword("");
368
+ inputHandler.setValue("");
369
+ onChange?.("");
370
+ }
371
+ }, 140);
372
+ } else {
373
+ setInputShowValue("");
374
+ serverSearchable && setKeyword("");
375
+ onChange?.("");
376
+ }
377
+ }
378
+
379
+ setTimeout(() => {
380
+ inputHandler.setFocus(false);
381
+ }, 100);
382
+
383
+ onBlur?.();
384
+ }}
385
+ onKeyDown={(e) => {
386
+ dataOptions?.length && onKeyDownOption(e);
387
+ }}
388
+ autoComplete="off"
389
+ autoFocus={autoFocus}
390
+ />
391
+
392
+
393
+ {(multiple && !searchable || (searchable && !inputHandler.focus)) && (
394
+ <div
395
+ className={`absolute top-1/2 -translate-y-1/2 overflow-x-auto py-1.5 input-scroll ${leftIcon ? "ml-[2.5rem]" : "ml-2"}`}
396
+ style={{ maxWidth: `calc(100% - ${leftIcon ? "5.2rem" : "3.2rem"})` }}
397
+ >
398
+ <div className={`input-values-container`}>
399
+ {multiple && typeof inputHandler.value != "string" && Array().concat(inputHandler.value)?.map((item, key) => {
400
+ return (
401
+ <div key={key} className={`input-values-item`}>
402
+ <span className="">{dataOptions?.find((option) => option.value == item)?.label}</span>
403
+ <FontAwesomeIcon
404
+ icon={faTimes}
405
+ className={`input-values-delete`}
406
+ onClick={() => {
407
+ const values = Array().concat(inputHandler.value);
408
+ const index = values.findIndex((val: string | number) => val == item);
409
+
410
+ inputHandler.setValue(values.filter((_, val) => val != index));
411
+
412
+ if (!values.filter((_, val) => val != index)?.length) {
413
+ setInputShowValue("");
414
+ serverSearchable && setKeyword("");
415
+ onChange?.("");
416
+ }
417
+ }}
418
+ />
419
+ </div>
420
+ );
421
+ })}
422
+ </div>
423
+ </div>
424
+ )}
425
+
426
+
427
+ {leftIcon && (
428
+ <FontAwesomeIcon
429
+ className={cn(
430
+ "left-4 input-icon ",
431
+ pcn<CT>(className, "icon"),
432
+ disabled && "opacity-60",
433
+ disabled && pcn<CT>(className, "icon", "disabled"),
434
+ inputHandler.focus && "text-primary",
435
+ inputHandler.focus && pcn<CT>(className, "icon", "focus")
436
+ )}
437
+ icon={leftIcon}
438
+ />
439
+ )}
440
+
441
+ {!multiple && clearable && inputHandler.value && (
442
+ <div
443
+ className={cn(
444
+ "right-12 input-icon cursor-pointer hover:text-danger",
445
+ disabled && "opacity-60 pointer-events-none",
446
+ disabled && pcn<CT>(className, "icon", "disabled")
447
+ )}
448
+ onClick={() => {
449
+ setInputShowValue("");
450
+ inputHandler.setValue("");
451
+ onChange?.("");
452
+ }}
453
+ >
454
+ <FontAwesomeIcon icon={faTimes} />
455
+ </div>
456
+ )}
457
+
458
+ <label
459
+ htmlFor={randomId}
460
+ className={cn(
461
+ "right-4 input-icon cursor-pointer hover:text-primary",
462
+ disabled && "opacity-60 pointer-events-none",
463
+ disabled && pcn<CT>(className, "icon", "disabled")
464
+ )}
465
+ >
466
+ <FontAwesomeIcon icon={faChevronDown} />
467
+ </label>
468
+ </div>
469
+
470
+ {!!dataOptions?.length && showOption && !loadingOption && !!filteredOptions?.length && (
471
+ <div>
472
+ <ul className={`input-suggest-container scroll-sm ${inputHandler.focus ? "opacity-100 scale-y-100" : "opacity-0 scale-y-0"}`}>
473
+ {(newOption ? [newOption, ...filteredOptions] : filteredOptions ).map((option, key) => {
474
+ const selected = !!((typeof inputHandler.value == "string" || typeof inputHandler.value == "number") && inputHandler.value == option.value) ||
475
+ (Array.isArray(inputHandler.value) && Array().concat(inputHandler.value).find((val: string | number) => val == option.value));
476
+
477
+ return (
478
+ <li
479
+ className={`
480
+ cursor-pointer hover:bg-light-primary
481
+ input-suggest
482
+ ${(key == activeOption || selected) && "bg-light-primary text-primary"}
483
+ `}
484
+ key={key}
485
+ onMouseDown={() => {
486
+ setKeydown(true);
487
+ setTimeout(() => inputHandler.setFocus(true), 110);
488
+ }}
489
+ onMouseUp={() => {
490
+ setKeydown(false);
491
+ setActiveOption(key);
492
+ setFilteredOptions([]);
493
+ setShowOption(false);
494
+
495
+ if (!multiple) {
496
+ setInputShowValue(option.label);
497
+ serverSearchable && setKeyword(option.label as string);
498
+ inputHandler.setValue(option.value);
499
+ onChange?.(option.value, option);
500
+ } else {
501
+ const values: string[] | number[] = Array.isArray(inputHandler.value)
502
+ ? Array().concat(inputHandler.value).filter((val) => val != option.value)
503
+ : [];
504
+
505
+ setInputShowValue("");
506
+ serverSearchable && setKeyword("");
507
+
508
+ if (
509
+ Array.isArray(inputHandler.value) && Array().concat(inputHandler.value).find((val) => val == option.value)
510
+ ) {
511
+ inputHandler.setValue(values);
512
+ onChange?.(values);
513
+ } else {
514
+ inputHandler.setValue([...Array().concat(values), option.value ]);
515
+ onChange?.([...Array().concat(values), option.value]);
516
+ }
517
+ }
518
+ setTimeout(() => inputHandler.setFocus(false), 120);
519
+ }}
520
+ >
521
+ {selected && (
522
+ <FontAwesomeIcon
523
+ icon={faCheck}
524
+ className="mr-2 text-sm"
525
+ />
526
+ )}
527
+ {option.label}
528
+ </li>
529
+ );
530
+ })}
531
+ </ul>
532
+ </div>
533
+ )}
534
+
535
+ {invalidMessage && (
536
+ <small className={cn("input-error-message", pcn<CT>(className, "error"))}>{invalidMessage}</small>
537
+ )}
538
+ </div>
539
+ </>
540
+ );
541
+ }