@skalfa/skalfa-component 1.0.7 → 1.0.8

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