@skalfa/skalfa-component 1.0.7 → 1.0.9

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 (107) hide show
  1. package/dist/accordion/Accordion.component.js +2 -3
  2. package/dist/breadcrumb/Breadcrumb.component.js +2 -3
  3. package/dist/button/Button.component.d.ts +2 -1
  4. package/dist/button/Button.component.js +2 -2
  5. package/dist/carousel/Carousel.component.js +2 -3
  6. package/dist/chip/Chip.component.js +2 -3
  7. package/dist/contexts-mock.d.ts +2 -0
  8. package/dist/contexts-mock.js +10 -0
  9. package/dist/input/Checkbox.component.js +2 -3
  10. package/dist/input/Input.component.d.ts +3 -2
  11. package/dist/input/Input.component.js +3 -3
  12. package/dist/input/InputCheckbox.component.js +1 -1
  13. package/dist/input/InputCurrency.component.d.ts +3 -2
  14. package/dist/input/InputCurrency.component.js +3 -3
  15. package/dist/input/InputDate.component.js +4 -5
  16. package/dist/input/InputDatetime.component.d.ts +3 -2
  17. package/dist/input/InputDatetime.component.js +3 -3
  18. package/dist/input/InputDocument.component.js +7 -8
  19. package/dist/input/InputImage.component.js +3 -4
  20. package/dist/input/InputNumber.component.js +3 -4
  21. package/dist/input/InputPassword.component.js +3 -3
  22. package/dist/input/InputRadio.component.js +1 -1
  23. package/dist/input/InputTime.component.js +3 -3
  24. package/dist/input/InputValues.component.js +3 -4
  25. package/dist/input/Select.component.js +6 -7
  26. package/dist/modal/FloatingPage.component.d.ts +3 -1
  27. package/dist/modal/FloatingPage.component.js +7 -3
  28. package/dist/modal/Modal.component.js +1 -2
  29. package/dist/modal/ModalConfirm.component.d.ts +2 -2
  30. package/dist/modal/ModalConfirm.component.js +14 -12
  31. package/dist/modal/Toast.component.js +1 -2
  32. package/dist/nav/Bottombar.component.d.ts +2 -2
  33. package/dist/nav/Bottombar.component.js +7 -8
  34. package/dist/nav/Footer.component.d.ts +2 -2
  35. package/dist/nav/Footer.component.js +6 -7
  36. package/dist/nav/Headbar.component.js +2 -3
  37. package/dist/nav/Navbar.component.d.ts +2 -2
  38. package/dist/nav/Navbar.component.js +3 -4
  39. package/dist/nav/Sidebar.component.d.ts +3 -7
  40. package/dist/nav/Sidebar.component.js +14 -17
  41. package/dist/supervision/FormSupervision.component.d.ts +4 -29
  42. package/dist/supervision/FormSupervision.component.js +84 -136
  43. package/dist/supervision/TableSupervision.component.d.ts +17 -26
  44. package/dist/supervision/TableSupervision.component.js +207 -193
  45. package/dist/table/ControlBar.component.js +15 -16
  46. package/dist/table/FilterComponent.js +4 -6
  47. package/dist/table/Pagination.component.js +2 -3
  48. package/dist/table/Table.component.js +3 -4
  49. package/dist/wrap/Swipe.component.js +2 -2
  50. package/package.json +18 -10
  51. package/src/accordion/Accordion.component.tsx +87 -0
  52. package/src/breadcrumb/Breadcrumb.component.tsx +79 -0
  53. package/src/button/Button.component.tsx +89 -0
  54. package/src/card/AlertCard.component.tsx +69 -0
  55. package/src/card/Card.component.tsx +25 -0
  56. package/src/card/DashboardCard.component.tsx +44 -0
  57. package/src/card/GalleryCard.component.tsx +50 -0
  58. package/src/card/ProductCard.component.tsx +65 -0
  59. package/src/card/ProfileCard.component.tsx +71 -0
  60. package/src/carousel/Carousel.component.tsx +111 -0
  61. package/src/chip/Chip.component.tsx +38 -0
  62. package/src/contexts-mock.ts +6 -0
  63. package/src/index.ts +70 -0
  64. package/src/input/Checkbox.component.tsx +102 -0
  65. package/src/input/Input.component.tsx +334 -0
  66. package/src/input/InputCheckbox.component.tsx +174 -0
  67. package/src/input/InputCurrency.component.tsx +165 -0
  68. package/src/input/InputDate.component.tsx +355 -0
  69. package/src/input/InputDatetime.component.tsx +267 -0
  70. package/src/input/InputDocument.component.tsx +360 -0
  71. package/src/input/InputImage.component.tsx +534 -0
  72. package/src/input/InputNumber.component.tsx +193 -0
  73. package/src/input/InputOtp.component.tsx +169 -0
  74. package/src/input/InputPassword.component.tsx +245 -0
  75. package/src/input/InputRadio.component.tsx +174 -0
  76. package/src/input/InputTime.component.tsx +280 -0
  77. package/src/input/InputValues.component.tsx +70 -0
  78. package/src/input/Radio.component.tsx +98 -0
  79. package/src/input/Select.component.tsx +557 -0
  80. package/src/modal/BottomSheet.component.tsx +246 -0
  81. package/src/modal/FloatingPage.component.tsx +133 -0
  82. package/src/modal/Modal.component.tsx +94 -0
  83. package/src/modal/ModalConfirm.component.tsx +204 -0
  84. package/src/modal/Toast.component.tsx +124 -0
  85. package/src/nav/Bottombar.component.tsx +71 -0
  86. package/src/nav/Footer.component.tsx +176 -0
  87. package/src/nav/Headbar.component.tsx +33 -0
  88. package/src/nav/Navbar.component.tsx +137 -0
  89. package/src/nav/Sidebar.component.tsx +307 -0
  90. package/src/nav/Tabbar.component.tsx +61 -0
  91. package/src/nav/Wizard.component.tsx +80 -0
  92. package/src/supervision/FormSupervision.component.tsx +404 -0
  93. package/src/supervision/TableSupervision.component.tsx +653 -0
  94. package/src/table/ControlBar.component.tsx +501 -0
  95. package/src/table/FilterComponent.tsx +517 -0
  96. package/src/table/Pagination.component.tsx +152 -0
  97. package/src/table/Table.component.tsx +436 -0
  98. package/src/typography/TypographyArticle.component.tsx +26 -0
  99. package/src/typography/TypographyColumn.component.tsx +20 -0
  100. package/src/typography/TypographyContent.component.tsx +20 -0
  101. package/src/typography/TypographyTips.component.tsx +20 -0
  102. package/src/wrap/Draggable.component.tsx +303 -0
  103. package/src/wrap/Image.component.tsx +10 -0
  104. package/src/wrap/OutsideClick.component.tsx +48 -0
  105. package/src/wrap/ScrollContainer.component.tsx +107 -0
  106. package/src/wrap/ShortcutProvider.tsx +57 -0
  107. package/src/wrap/Swipe.component.tsx +121 -0
@@ -0,0 +1,517 @@
1
+ "use client"
2
+
3
+ import { useState, useEffect } from "react";
4
+ import { ApiFilterType, cn, pcn, useResponsive } from "@utils";
5
+ import { ButtonComponent } from "../button/Button.component";
6
+ import { ChipComponent } from "../chip/Chip.component";
7
+ import { InputComponent } from "../input/Input.component";
8
+ import { InputCurrencyComponent } from "../input/InputCurrency.component";
9
+ import { InputDateComponent } from "../input/InputDate.component";
10
+ import { InputNumberComponent } from "../input/InputNumber.component";
11
+ import { ModalComponent } from "../modal/Modal.component";
12
+ import { SelectComponent } from "../input/Select.component";
13
+ import { useToggleContext } from "@contexts";
14
+
15
+ type CT = "base" | "title";
16
+
17
+ export type FilterColumnOption = {
18
+ label: string;
19
+ selector: string;
20
+ type: "text" | "number" | "currency" | "date";
21
+ } | {
22
+ label: string;
23
+ selector: string;
24
+ type: "select";
25
+ options: { label: string; value: any }[];
26
+ };
27
+
28
+ export type FilterBookmark = {
29
+ id: string;
30
+ name: string;
31
+ filters: ApiFilterType[];
32
+ createdAt: number;
33
+ };
34
+
35
+ export interface AdvancedFilterProps {
36
+ columns : FilterColumnOption[];
37
+ onChange ?: (filters: ApiFilterType[]) => void;
38
+ value ?: ApiFilterType[];
39
+ onMinimize ?: () => void;
40
+
41
+ /** Use custom class with: "title::". */
42
+ className ?: string;
43
+ }
44
+
45
+ const FILTER_OPERATORS = [
46
+ { label: "Cari", value: "li" },
47
+ { label: "Sama", value: "eq" },
48
+ { label: "Tidak Sama", value: "ne" },
49
+ { label: "Termasuk", value: "in" },
50
+ { label: "Tidak Termasuk", value: "ni" },
51
+ { label: "Antara", value: "bw" },
52
+ ];
53
+
54
+ const LOGIC_OPTIONS = [
55
+ { label: "Dan", value: "and" },
56
+ { label: "Atau", value: "or" },
57
+ ];
58
+
59
+ const BOOKMARK_KEY = "filter-bookmarks";
60
+
61
+ export function FilterComponent({
62
+ columns,
63
+ onChange,
64
+ value,
65
+ onMinimize,
66
+ className = "",
67
+ }: AdvancedFilterProps) {
68
+ const { isSm } = useResponsive();
69
+ const { toggle, setToggle } = useToggleContext()
70
+ const [filters, setFilters] = useState<ApiFilterType[]>([]);
71
+
72
+ const [bookmarks, setBookmarks] = useState<FilterBookmark[]>([]);
73
+
74
+ const handleChange = (index: number, key: keyof ApiFilterType, value: any) => {
75
+ const updated = [...filters];
76
+ (updated[index] as any)[key] = value;
77
+
78
+ if (key === "column") {
79
+ updated[index].type = "";
80
+ updated[index].value = "";
81
+ }
82
+
83
+ setFilters(updated);
84
+ };
85
+
86
+ const addFilter = () => {
87
+ setFilters([
88
+ ...filters,
89
+ { column: "", type: "", value: "", logic: filters.length ? "and" : undefined },
90
+ ]);
91
+ };
92
+
93
+ const removeFilter = (index: number) => {
94
+ const updated = filters.filter((_, i) => i !== index);
95
+ setFilters(updated);
96
+ };
97
+
98
+ useEffect(() => {
99
+ if (onChange) onChange(filters);
100
+ }, [filters]);
101
+
102
+
103
+ useEffect(() => {}, [value]);
104
+
105
+
106
+ useEffect(() => {
107
+ setBookmarks(JSON.parse(localStorage.getItem(BOOKMARK_KEY) || "[]"));
108
+ }, []);
109
+
110
+ useEffect(() => {
111
+ localStorage.setItem(BOOKMARK_KEY, JSON.stringify(bookmarks));
112
+ }, [bookmarks]);
113
+
114
+
115
+ return (
116
+ <>
117
+ <div className={cn("filter-panel", pcn<CT>(className, "base"))}>
118
+ <div className="filter-panel-header">
119
+ <p className={cn(pcn<CT>(className, "title"))}>Filter</p>
120
+ <div className="filter-panel-controls">
121
+ <div className="filter-panel-bookmarks-wrapper">
122
+ <ButtonComponent
123
+ icon="solid/bookmark"
124
+ variant="outline"
125
+ paint="primary"
126
+ className="filter-panel-bookmark-btn icon::text-slate-400"
127
+ size="xs"
128
+ onClick={() => setToggle("MODAL_BOOKMARK", { bookmark_id: "new" })}
129
+ />
130
+
131
+ {!!bookmarks?.length && (
132
+ <ChipComponent
133
+ items={bookmarks.slice(0, 5).map((b) => b.name)}
134
+ onClick={(_, index) => setFilters(bookmarks[index]?.filters)}
135
+ onDelete={(_, index) => setBookmarks(bookmarks.filter(( _, bi: number) => bi != index))}
136
+ />
137
+ )}
138
+
139
+ {bookmarks?.length > 5 && (
140
+ <ButtonComponent
141
+ icon="solid/ellipsis-h"
142
+ variant="outline"
143
+ paint="primary"
144
+ className="filter-panel-bookmark-btn icon::text-slate-400"
145
+ size="xs"
146
+ onClick={() => setToggle("MODAL_BOOKMARK_LIST")}
147
+ />
148
+ )}
149
+ </div>
150
+
151
+ <ButtonComponent
152
+ icon="solid/rotate"
153
+ label="Bersihkan"
154
+ variant="outline"
155
+ paint="primary"
156
+ className="filter-panel-bookmark-btn icon::text-slate-400"
157
+ size="xs"
158
+ onClick={() => setFilters([])}
159
+ />
160
+
161
+ {onMinimize && (
162
+ <ButtonComponent
163
+ icon="solid/chevron-up"
164
+ variant="outline"
165
+ paint="primary"
166
+ className="filter-panel-bookmark-btn icon::text-slate-400"
167
+ size="xs"
168
+ onClick={onMinimize}
169
+ />
170
+ )}
171
+ </div>
172
+ </div>
173
+
174
+ {filters.map((f, i) => {
175
+ const column = columns.find((c) => c.selector === f.column);
176
+
177
+ return (
178
+ <div key={i} className="filter-row">
179
+ {i > 0 && (
180
+ <div className="filter-col-logic">
181
+ <SelectComponent
182
+ name={`filter_logic_${i}`}
183
+ options={LOGIC_OPTIONS}
184
+ placeholder="Dan/Atau"
185
+ value={f.logic}
186
+ onChange={(e) => handleChange(i, "logic", e as "and" | "or")}
187
+ className="filter-input-field"
188
+ />
189
+ </div>
190
+ )}
191
+
192
+ <div className={i > 0 ? "filter-col-column-with-logic" : "filter-col-column"}>
193
+ <SelectComponent
194
+ name={`filter_column_${i}`}
195
+ options={columns.map((c) => ({
196
+ label: c.label,
197
+ value: c.selector,
198
+ }))}
199
+ placeholder="Kolom"
200
+ value={f.column}
201
+ onChange={(e) => handleChange(i, "column", e)}
202
+ className="filter-input-field"
203
+ />
204
+ </div>
205
+
206
+ <div className="filter-col-operator">
207
+ <SelectComponent
208
+ name={`filter_operator_${i}`}
209
+ options={FILTER_OPERATORS}
210
+ placeholder="Operator"
211
+ value={f.type}
212
+ onChange={(e) => handleChange(i, "type", e)}
213
+ className="filter-input-field"
214
+ />
215
+ </div>
216
+
217
+ {isSm && (
218
+ <ButtonComponent
219
+ icon="solid/times"
220
+ paint="danger"
221
+ variant="outline"
222
+ onClick={() => removeFilter(i)}
223
+ size="xs"
224
+ />
225
+ )}
226
+
227
+ {column && (
228
+ <div className="filter-col-value">
229
+ <InputFilterValueComponent
230
+ type={column.type}
231
+ name={`filter_value_${i}`}
232
+ placeholder="..."
233
+ value={f.value || ""}
234
+ onChange={(e: any) => handleChange(i, "value", e)}
235
+ options={column.type === "select" && column.options ? column.options : []}
236
+ className="filter-input-field"
237
+ between={f.type === "bw"}
238
+ multiple={["in", "ni"].includes(f.type || "")}
239
+ />
240
+ </div>
241
+ )}
242
+
243
+ {!isSm && (
244
+ <ButtonComponent
245
+ icon="solid/times"
246
+ paint="danger"
247
+ variant="outline"
248
+ onClick={() => removeFilter(i)}
249
+ size="xs"
250
+ />
251
+ )}
252
+ </div>
253
+ );
254
+ })}
255
+
256
+ <ButtonComponent
257
+ label="Tambahkan"
258
+ icon="solid/plus"
259
+ variant="outline"
260
+ paint="primary"
261
+ size={isSm ? "xs" : "sm"}
262
+ onClick={addFilter}
263
+ className="filter-add-btn"
264
+ />
265
+ </div>
266
+
267
+ <ModalComponent
268
+ show={!!toggle["MODAL_BOOKMARK_LIST"]}
269
+ onClose={() => setToggle("MODAL_BOOKMARK_LIST", false)}
270
+ title="Bookmark Filter"
271
+ >
272
+ <div className="filter-bookmark-list-container">
273
+ {bookmarks?.length ? bookmarks?.map((b, key) => (
274
+ <div className="filter-bookmark-row" key={key}>
275
+ <p className="filter-bookmark-name">{b.name}</p>
276
+ <div className="flex gap-2">
277
+ <ButtonComponent
278
+ icon="solid/plus"
279
+ variant="outline"
280
+ paint="primary"
281
+ className=""
282
+ size="xs"
283
+ onClick={() => {
284
+ setFilters(b.filters)
285
+ setToggle("MODAL_BOOKMARK_LIST", false)
286
+ }}
287
+ />
288
+ <ButtonComponent
289
+ icon="solid/times"
290
+ variant="outline"
291
+ paint="danger"
292
+ className=""
293
+ size="xs"
294
+ onClick={() => setBookmarks(bookmarks.filter(( _, bi: number) => bi != key))}
295
+ />
296
+ </div>
297
+ </div>
298
+ )) : (
299
+ <div className="filter-bookmark-empty-text"> -- Tidak ada Bookmark --</div>
300
+ )}
301
+ </div>
302
+ </ModalComponent>
303
+
304
+ <ModalComponent
305
+ show={!!toggle["MODAL_BOOKMARK"]}
306
+ onClose={() => setToggle("MODAL_BOOKMARK", false)}
307
+ title="Bookmark Filter"
308
+ footer={
309
+ <div className="flex justify-end">
310
+ <ButtonComponent
311
+ label="Terapkan"
312
+ onClick={() => {
313
+ let updated: FilterBookmark[] = [];
314
+
315
+ if ((toggle["MODAL_BOOKMARK"] as { bookmark_id: string })?.bookmark_id == "new") {
316
+ const newBookmark: FilterBookmark = {
317
+ id: crypto.randomUUID(),
318
+ name: (toggle["MODAL_BOOKMARK"] as { bookmark_name: string })?.bookmark_name || "Tanpa Nama",
319
+ filters,
320
+ createdAt: Date.now(),
321
+ };
322
+
323
+ updated = [newBookmark, ...bookmarks];
324
+ } else {
325
+ updated = bookmarks.map(b =>
326
+ b.id === (toggle["MODAL_BOOKMARK"] as { bookmark_id: string })?.bookmark_id
327
+ ? { ...b, filters, createdAt: Date.now() }
328
+ : b
329
+ );
330
+ }
331
+
332
+ setBookmarks(updated)
333
+ setToggle("MODAL_BOOKMARK", false)
334
+ }}
335
+ />
336
+ </div>
337
+ }
338
+ >
339
+ <div className="filter-bookmark-form-container">
340
+ <SelectComponent
341
+ name={`bookmark_id`}
342
+ placeholder="Pilih bookmark filter..."
343
+ value={(toggle["MODAL_BOOKMARK"] as { bookmark_id: string })?.bookmark_id ?? ""}
344
+ onChange={e => setToggle("MODAL_BOOKMARK", {...(toggle["MODAL_BOOKMARK"] as object), bookmark_id: e})}
345
+ options={[
346
+ {
347
+ label: "-- BOOKMARK BARU --",
348
+ value: "new",
349
+ },
350
+ ...bookmarks.map((b) => ({
351
+ label: b.name,
352
+ value: b.id,
353
+ }))
354
+ ]}
355
+ />
356
+
357
+ {(toggle["MODAL_BOOKMARK"] as { bookmark_id: string })?.bookmark_id == "new" && (
358
+ <InputComponent
359
+ name="bookmark_name"
360
+ placeholder="Masukkan nama bookmark..."
361
+ value={(toggle["MODAL_BOOKMARK"] as { bookmark_name: string })?.bookmark_name ?? ""}
362
+ onChange={e => setToggle("MODAL_BOOKMARK", {...(toggle["MODAL_BOOKMARK"] as object), bookmark_name: e})}
363
+ className="filter-bookmark-form-input"
364
+ />
365
+ )}
366
+ </div>
367
+ </ModalComponent>
368
+ </>
369
+ );
370
+ }
371
+
372
+ interface FilterInputProps {
373
+ type ?: string | null;
374
+ name : string;
375
+ value : string | number | number[] | string[] | null;
376
+ onChange : (value: any) => void;
377
+ options ?: { label: string; value: any }[];
378
+ placeholder ?: string;
379
+ className ?: string;
380
+ between ?: boolean;
381
+ multiple ?: boolean;
382
+ }
383
+
384
+ export function InputFilterValueComponent({
385
+ type,
386
+ name,
387
+ value,
388
+ onChange,
389
+ options = [],
390
+ placeholder = "",
391
+ className = "",
392
+ between,
393
+ multiple,
394
+ }: FilterInputProps) {
395
+ const resolvedType = type || "text";
396
+
397
+ if (!between) {
398
+ switch (resolvedType) {
399
+ case "select":
400
+ return (
401
+ <SelectComponent
402
+ name={name}
403
+ options={options}
404
+ placeholder={placeholder}
405
+ value={options.find((o) => o.value === value)?.label}
406
+ onChange={onChange}
407
+ className={className}
408
+ />
409
+ );
410
+
411
+ case "number":
412
+ return (
413
+ <InputNumberComponent
414
+ name={name}
415
+ value={Number(value) || 0}
416
+ onChange={onChange}
417
+ placeholder={placeholder}
418
+ className={className}
419
+ multiple={multiple}
420
+ />
421
+ );
422
+
423
+ case "currency":
424
+ return (
425
+ <InputCurrencyComponent
426
+ name={name}
427
+ value={Number(value) || 0}
428
+ onChange={onChange}
429
+ placeholder={placeholder}
430
+ className={className}
431
+ />
432
+ );
433
+
434
+ case "date":
435
+ return (
436
+ <InputDateComponent
437
+ name={name}
438
+ value={value as string || ""}
439
+ onChange={onChange}
440
+ placeholder={placeholder}
441
+ className={className}
442
+ />
443
+ );
444
+
445
+ default:
446
+ return (
447
+ <InputComponent
448
+ name={name}
449
+ value={value || ""}
450
+ onChange={onChange}
451
+ placeholder={placeholder}
452
+ className={className}
453
+ multiple={multiple}
454
+ />
455
+ );
456
+ }
457
+ }
458
+
459
+ const val = Array.isArray(value) ? value : ["", ""];
460
+ const [from, to] = val;
461
+
462
+ const renderInput = (pos: "from" | "to") => {
463
+ const currentValue = pos === "from" ? from : to;
464
+ const handle = (v: any) => onChange(pos === "from" ? [v, to] : [from, v]);
465
+
466
+ switch (resolvedType) {
467
+ case "number":
468
+ return (
469
+ <InputNumberComponent
470
+ name={`${name}_${pos}`}
471
+ value={Number(currentValue) || 0}
472
+ onChange={handle}
473
+ placeholder={pos === "from" ? "Dari" : "Sampai"}
474
+ className={className}
475
+ />
476
+ );
477
+ case "currency":
478
+ return (
479
+ <InputCurrencyComponent
480
+ name={`${name}_${pos}`}
481
+ value={Number(currentValue) || 0}
482
+ onChange={handle}
483
+ placeholder={pos === "from" ? "Dari" : "Sampai"}
484
+ className={className}
485
+ />
486
+ );
487
+ case "date":
488
+ return (
489
+ <InputDateComponent
490
+ name={`${name}_${pos}`}
491
+ value={currentValue as string || ""}
492
+ onChange={handle}
493
+ placeholder={pos === "from" ? "Dari" : "Sampai"}
494
+ className={className}
495
+ />
496
+ );
497
+ default:
498
+ return (
499
+ <InputComponent
500
+ name={`${name}_${pos}`}
501
+ value={currentValue || ""}
502
+ onChange={(e: any) => handle(e.target.value)}
503
+ placeholder={pos === "from" ? "Dari" : "Sampai"}
504
+ className={className}
505
+ />
506
+ );
507
+ }
508
+ };
509
+
510
+ return (
511
+ <div className="filter-between-container">
512
+ {renderInput("from")}
513
+ <span>s/d</span>
514
+ {renderInput("to")}
515
+ </div>
516
+ );
517
+ }
@@ -0,0 +1,152 @@
1
+ "use client"
2
+ import { Icon, type IconName } from "@skalfa/skalfa-icon";
3
+
4
+
5
+ import { useEffect, useState } from "react";
6
+ import { cn, pcn } from "@utils";
7
+ import { ButtonComponent } from "../button/Button.component";
8
+ import { InputRadioComponent } from "../input/InputRadio.component";
9
+
10
+ type CT = "item" | "active" | "base";
11
+
12
+ export interface PaginationProps {
13
+ totalRow : number;
14
+ paginate : number;
15
+ page : number;
16
+ onChange ?: (totalRow: number, paginate: number, page: number) => void;
17
+ className ?: string;
18
+ }
19
+
20
+ export function PaginationComponent({
21
+ paginate = 10,
22
+ page = 1,
23
+ totalRow = 0,
24
+ onChange,
25
+ className = "",
26
+ }: PaginationProps) {
27
+ const [pages, setPages] = useState<number[]>([]);
28
+ const lastPage = Math.ceil(totalRow / paginate);
29
+
30
+ useEffect(() => {
31
+ let newPages = [];
32
+ if (totalRow > paginate) {
33
+ const start = Math.max(1, page - 1);
34
+ const end = Math.min(lastPage, page + 1);
35
+ newPages = Array.from({ length: end - start + 1 }, (_, i) => start + i);
36
+ } else {
37
+ newPages = [1];
38
+ }
39
+ setPages(newPages);
40
+ }, [totalRow, page, paginate]);
41
+
42
+ if (!totalRow) return null;
43
+
44
+ return (
45
+ <div className={cn("pagination-container", pcn<CT>(className, "base"))}>
46
+ <div className="pagination-desktop-wrapper">
47
+ <InputRadioComponent
48
+ name="paginate"
49
+ options={[10, 20, 50].map((val) => ({
50
+ value: val,
51
+ label: String(val),
52
+ }))}
53
+ value={paginate}
54
+ onChange={(e) => onChange?.(totalRow, Number(e), 1)}
55
+ className="pagination-paginate-select"
56
+ />
57
+
58
+ {totalRow > paginate && (
59
+ <div className="pagination-desktop-pages">
60
+ {page > 1 && (
61
+ <button
62
+ className="pagination-overflow"
63
+ onClick={() => onChange?.(totalRow, paginate, page - 1)}
64
+ >
65
+ <Icon icon="solid/chevron-left" />
66
+ </button>
67
+ )}
68
+
69
+ {page > 2 && (
70
+ <>
71
+ <button
72
+ className={cn("pagination-item", pcn<CT>(className, "item"))}
73
+ onClick={() => onChange?.(totalRow, paginate, 1)}
74
+ >
75
+ 1
76
+ </button>
77
+ {page > 3 && <span className="pagination-overflow">...</span>}
78
+ </>
79
+ )}
80
+
81
+ {pages.map((p) => (
82
+ <button
83
+ key={p}
84
+ className={cn(
85
+ "pagination-item",
86
+ pcn<CT>(className, "item"),
87
+ p === page && cn("pagination-item-active", pcn<CT>(className, "active"))
88
+ )}
89
+ onClick={() => onChange?.(totalRow, paginate, p)}
90
+ >
91
+ {p}
92
+ </button>
93
+ ))}
94
+
95
+ {page < lastPage - 1 && (
96
+ <>
97
+ {page < lastPage - 2 && (
98
+ <span className="pagination-overflow">...</span>
99
+ )}
100
+ <button
101
+ className={cn("pagination-item", pcn<CT>(className, "item"))}
102
+ onClick={() => onChange?.(totalRow, paginate, lastPage)}
103
+ >
104
+ {lastPage}
105
+ </button>
106
+ </>
107
+ )}
108
+
109
+ {page < lastPage && (
110
+ <button
111
+ className="pagination-overflow"
112
+ onClick={() => onChange?.(totalRow, paginate, page + 1)}
113
+ >
114
+ <Icon icon="solid/chevron-right" />
115
+ </button>
116
+ )}
117
+ </div>
118
+ )}
119
+ </div>
120
+
121
+ <div className="pagination-desktop-info">
122
+ {Math.min(totalRow, paginate * (page - 1) + 1)} - {Math.min(totalRow, paginate * page)} / {totalRow}
123
+ </div>
124
+
125
+
126
+ <div className="pagination-mobile-wrapper">
127
+ <div className="pagination-mobile-info">
128
+ {Math.min(totalRow, paginate * (page - 1) + 1)} - {Math.min(totalRow, paginate * page)} / {totalRow}
129
+ </div>
130
+
131
+ {totalRow > paginate && (
132
+ <div className="pagination-mobile-controls">
133
+ {page > 1 && (
134
+ <ButtonComponent
135
+ icon="solid/chevron-left"
136
+ onClick={() => onChange?.(totalRow, paginate, page - 1)}
137
+ size="sm"
138
+ />
139
+ )}
140
+ {page < lastPage && (
141
+ <ButtonComponent
142
+ icon="solid/chevron-right"
143
+ onClick={() => onChange?.(totalRow, paginate, page + 1)}
144
+ size="sm"
145
+ />
146
+ )}
147
+ </div>
148
+ )}
149
+ </div>
150
+ </div>
151
+ );
152
+ }