@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,163 @@
1
+ "use client"
2
+
3
+ import { InputHTMLAttributes, ReactNode } from "react";
4
+ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
5
+ import { cn, conversion, pcn, useInputHandler, useInputRandomId, useValidation, validation, ValidationRules } from "@utils";
6
+
7
+
8
+
9
+ type CT = "label" | "tip" | "error" | "input" | "icon";
10
+
11
+ export interface InputCurrencyProps extends Omit<InputHTMLAttributes<HTMLInputElement>, "onChange"> {
12
+ label ?: string;
13
+ tip ?: string | ReactNode;
14
+ leftIcon ?: any;
15
+ rightIcon ?: any;
16
+
17
+ value ?: number;
18
+ invalid ?: string;
19
+ validations ?: ValidationRules;
20
+ format ?: {
21
+ locale ?: string;
22
+ currency ?: string;
23
+ };
24
+
25
+ onChange ?: (value: number) => any;
26
+ register ?: (name: string, validations?: ValidationRules) => void;
27
+ unregister ?: (name: string) => void;
28
+
29
+ /** Use custom class with: "label::", "tip::", "error::", "icon::". */
30
+ className ?: string;
31
+ }
32
+
33
+
34
+
35
+ export function InputCurrencyComponent({
36
+ label,
37
+ tip,
38
+ leftIcon,
39
+ rightIcon,
40
+
41
+ value,
42
+ invalid,
43
+
44
+ validations,
45
+ format,
46
+
47
+ register,
48
+ unregister,
49
+ onChange,
50
+
51
+ className = "",
52
+ ...props
53
+ }: InputCurrencyProps) {
54
+
55
+ // =========================>
56
+ // ## Initial
57
+ // =========================>
58
+ const inputHandler = useInputHandler(props.name, value, validations, register, unregister, false)
59
+ const randomId = useInputRandomId()
60
+
61
+
62
+ // =========================>
63
+ // ## Invalid handler
64
+ // =========================>
65
+ const [invalidMessage] = useValidation(inputHandler.value, validations, invalid, inputHandler.idle);
66
+
67
+
68
+ return (
69
+ <div className="relative flex flex-col gap-y-0.5">
70
+ <label
71
+ htmlFor={randomId}
72
+ className={cn(
73
+ "input-label",
74
+ pcn<CT>(className, "label"),
75
+ props.disabled && "opacity-50",
76
+ props.disabled && pcn<CT>(className, "label", "disabled"),
77
+ inputHandler.focus && "text-primary",
78
+ inputHandler.focus && pcn<CT>(className, "label", "focus"),
79
+ !!invalidMessage && "text-danger",
80
+ !!invalidMessage && pcn<CT>(className, "label", "focus"),
81
+ )}
82
+ >
83
+ {label}
84
+ {validations && validation.hasRules(validations, "required") && <span className="text-danger ml-1">*</span>}
85
+ </label>
86
+
87
+ {tip && (
88
+ <small
89
+ className={cn(
90
+ "input-tip",
91
+ pcn<CT>(className, "tip"),
92
+ props.disabled && "opacity-60",
93
+ props.disabled && pcn<CT>(className, "tip", "disabled"),
94
+ )}
95
+ >{tip}</small>
96
+ )}
97
+
98
+ <div className="relative">
99
+ <input
100
+ {...props}
101
+ id={randomId}
102
+ className={cn(
103
+ "input",
104
+ leftIcon && "pl-12",
105
+ rightIcon && "pr-12",
106
+ pcn<CT>(className, "input"),
107
+ !!invalidMessage && "input-error",
108
+ !!invalidMessage && pcn<CT>(className, "input", "error"),
109
+ )}
110
+ value={inputHandler.value ? conversion.currency(inputHandler.value, format?.locale, format?.currency) : ""}
111
+ onChange={(e) => {
112
+ const val = Number(e.target.value.replace(/[^0-9]/g,""));
113
+
114
+ inputHandler.setValue(val);
115
+ inputHandler.setIdle(false);
116
+
117
+ onChange?.(val);
118
+ }}
119
+ onFocus={(e) => {
120
+ props.onFocus?.(e);
121
+ inputHandler.setFocus(true);
122
+ }}
123
+ onBlur={(e) => {
124
+ props.onBlur?.(e);
125
+ setTimeout(() => inputHandler.setFocus(false), 100);
126
+ }}
127
+ autoComplete="off"
128
+ />
129
+
130
+ {leftIcon && (
131
+ <FontAwesomeIcon
132
+ className={cn(
133
+ "left-4 input-icon",
134
+ pcn<CT>(className, "icon"),
135
+ props.disabled && "opacity-60",
136
+ props.disabled && pcn<CT>(className, "icon", "disabled"),
137
+ inputHandler.focus && "text-primary",
138
+ inputHandler.focus && pcn<CT>(className, "icon", "focus"),
139
+ )}
140
+ icon={leftIcon}
141
+ />
142
+ )}
143
+ {rightIcon && (
144
+ <FontAwesomeIcon
145
+ className={cn(
146
+ "right-4 input-icon",
147
+ pcn<CT>(className, "icon"),
148
+ props.disabled && "opacity-60",
149
+ props.disabled && pcn<CT>(className, "icon", "disabled"),
150
+ inputHandler.focus && "text-primary",
151
+ inputHandler.focus && pcn<CT>(className, "icon", "focus"),
152
+ )}
153
+ icon={rightIcon}
154
+ />
155
+ )}
156
+ </div>
157
+
158
+ {invalidMessage && (
159
+ <small className={cn("input-error-message", pcn<CT>(className, "error"))}>{invalidMessage}</small>
160
+ )}
161
+ </div>
162
+ );
163
+ }
@@ -0,0 +1,352 @@
1
+ "use client"
2
+
3
+ import { InputHTMLAttributes, ReactNode, useEffect, useMemo, useRef, useState } from "react";
4
+ import moment from "moment";
5
+ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
6
+ import { faChevronLeft, faChevronRight } from "@fortawesome/free-solid-svg-icons";
7
+ import { cn, pcn, useInputHandler, useInputRandomId, useResponsive, useValidation, validation, ValidationRules } from "@utils";
8
+ import { BottomSheetComponent, ButtonComponent, OutsideClickComponent } from "@components";
9
+
10
+
11
+
12
+ type CT = "label" | "tip" | "error" | "input" | "icon";
13
+
14
+ export interface InputDateProps extends Omit<InputHTMLAttributes<HTMLInputElement>, "onChange"> {
15
+ label ?: string;
16
+ tip ?: string | ReactNode;
17
+ leftIcon ?: any;
18
+ rightIcon ?: any;
19
+
20
+ value ?: string;
21
+ invalid ?: string;
22
+ validations ?: ValidationRules;
23
+
24
+ onChange ?: (value: string) => any;
25
+ register ?: (name: string, validations?: ValidationRules) => void;
26
+ unregister ?: (name: string) => void;
27
+
28
+ /** Use custom class with: "label::", "tip::", "error::", "icon::". */
29
+ className ?: string;
30
+ }
31
+
32
+
33
+
34
+ export function InputDateComponent({
35
+ label,
36
+ tip,
37
+ leftIcon,
38
+ rightIcon,
39
+
40
+ value,
41
+ invalid,
42
+ validations,
43
+
44
+ register,
45
+ unregister,
46
+ onChange,
47
+
48
+ className = "",
49
+ ...props
50
+ }: InputDateProps) {
51
+ const { isSm } = useResponsive();
52
+
53
+ // =========================>
54
+ // ## Initial
55
+ // =========================>
56
+ const inputHandler = useInputHandler(props.name, value, validations, register, unregister, false)
57
+ const randomId = useInputRandomId()
58
+
59
+
60
+ // =========================>
61
+ // ## Invalid handler
62
+ // =========================>
63
+ const [invalidMessage] = useValidation(inputHandler.value, validations, invalid, inputHandler.idle);
64
+
65
+
66
+ return (
67
+ <>
68
+ <div className="relative flex flex-col gap-y-0.5">
69
+ <label
70
+ htmlFor={randomId}
71
+ className={cn(
72
+ "input-label",
73
+ pcn<CT>(className, "label"),
74
+ props.disabled && "opacity-50",
75
+ props.disabled && pcn<CT>(className, "label", "disabled"),
76
+ inputHandler.focus && "text-primary",
77
+ inputHandler.focus && pcn<CT>(className, "label", "focus"),
78
+ !!invalidMessage && "text-danger",
79
+ !!invalidMessage && pcn<CT>(className, "label", "focus")
80
+ )}
81
+ >
82
+ {label}
83
+ {validations && validation.hasRules(validations, "required") && <span className="text-danger ml-1">*</span>}
84
+ </label>
85
+
86
+ {tip && (
87
+ <small
88
+ className={cn(
89
+ "input-tip",
90
+ pcn<CT>(className, "tip"),
91
+ props.disabled && "opacity-60",
92
+ props.disabled && pcn<CT>(className, "tip", "disabled")
93
+ )}
94
+ >{tip}</small>
95
+ )}
96
+
97
+ <OutsideClickComponent onOutsideClick={!isSm ? () => inputHandler.setFocus(false) : undefined}>
98
+ <div className="relative">
99
+ <input
100
+ {...props}
101
+ id={randomId}
102
+ className={cn(
103
+ "input",
104
+ leftIcon && "pl-12",
105
+ rightIcon && "pr-12",
106
+ pcn<CT>(className, "input"),
107
+ !!invalidMessage && "input-error",
108
+ !!invalidMessage && pcn<CT>(className, "input", "error")
109
+ )}
110
+ value={inputHandler.value}
111
+ onChange={(e) => {
112
+ inputHandler.setValue(e.target.value);
113
+ inputHandler.setValue(false);
114
+ onChange?.(e.target.value);
115
+ }}
116
+ onFocus={(e) => {
117
+ props.onFocus?.(e);
118
+ inputHandler.setFocus(true);
119
+ }}
120
+ onBlur={(e) => {
121
+ props.onBlur?.(e);
122
+ }}
123
+ autoComplete="off"
124
+ inputMode={isSm ? "none" : undefined}
125
+ />
126
+
127
+ {leftIcon && (
128
+ <FontAwesomeIcon
129
+ className={cn(
130
+ "left-4 input-icon ",
131
+ pcn<CT>(className, "icon"),
132
+ props.disabled && "opacity-60",
133
+ props.disabled && pcn<CT>(className, "icon", "disabled"),
134
+ inputHandler.focus && "text-primary",
135
+ inputHandler.focus && pcn<CT>(className, "icon", "focus")
136
+ )}
137
+ icon={leftIcon}
138
+ />
139
+ )}
140
+
141
+ {rightIcon && (
142
+ <FontAwesomeIcon
143
+ className={cn(
144
+ "right-4 input-icon",
145
+ pcn<CT>(className, "icon"),
146
+ props.disabled && "opacity-60",
147
+ props.disabled && pcn<CT>(className, "icon", "disabled"),
148
+ inputHandler.focus && "text-primary",
149
+ inputHandler.focus && pcn<CT>(className, "icon", "focus")
150
+ )}
151
+ icon={rightIcon}
152
+ />
153
+ )}
154
+
155
+ {!isSm && inputHandler.focus && (
156
+ <div className="w-max h-72 bg-background border p-2 rounded-[6px] absolute top-full right-0 mt-1 z-50">
157
+ <InputDatePickerComponent
158
+ onChange={(e) => {
159
+ inputHandler.setValue(e);
160
+ onChange?.(e);
161
+ }}
162
+ />
163
+ </div>
164
+ )}
165
+ </div>
166
+ </OutsideClickComponent>
167
+
168
+ {invalidMessage && (
169
+ <small className={cn("input-error-message", pcn<CT>(className, "error"))}>{invalidMessage}</small>
170
+ )}
171
+ </div>
172
+
173
+ {isSm && (
174
+ <BottomSheetComponent
175
+ show={inputHandler.focus}
176
+ onClose={() => inputHandler.setFocus(false)}
177
+ size={380}
178
+ footer={
179
+ <div className="p-4">
180
+ <ButtonComponent
181
+ label="Selesai"
182
+ variant="outline"
183
+ onClick={() => inputHandler.setFocus(false)}
184
+ block
185
+ />
186
+ </div>
187
+ }
188
+ >
189
+ <div className="p-4">
190
+ <InputDatePickerComponent
191
+ onChange={(e) => {
192
+ inputHandler.setValue(e);
193
+ onChange?.(e);
194
+ }}
195
+ />
196
+ </div>
197
+ </BottomSheetComponent>
198
+ )}
199
+ </>
200
+ );
201
+ }
202
+
203
+
204
+
205
+ export interface InputDatePickerProps {
206
+ onChange ?: (date: string) => void;
207
+ minDate ?: string;
208
+ maxDate ?: string;
209
+ rightElement ?: ReactNode;
210
+ };
211
+
212
+
213
+
214
+ export const InputDatePickerComponent: React.FC<InputDatePickerProps> = ({
215
+ onChange,
216
+ minDate,
217
+ maxDate,
218
+ rightElement,
219
+ }) => {
220
+ const activeYearRef = useRef<HTMLDivElement | null>(null);
221
+ const containerYearRef = useRef<HTMLDivElement | null>(null);
222
+
223
+ const [currentDate, setCurrentDate] = useState(moment());
224
+ const [selectedDate, setSelectedDate] = useState(moment());
225
+
226
+ const startDate = moment(currentDate).startOf("month").startOf("week");
227
+ const endDate = moment(currentDate).endOf("month").endOf("week");
228
+
229
+ const handlePrevMonth = () => setCurrentDate(moment(currentDate).subtract(1, "month"));
230
+ const handleNextMonth = () => setCurrentDate(moment(currentDate).add(1, "month"));
231
+
232
+ const handleDateClick = (date: moment.Moment) => {
233
+ if ((minDate && date.isBefore(moment(minDate))) || (maxDate && date.isAfter(moment(maxDate)))) { return; }
234
+
235
+ setSelectedDate(date);
236
+ onChange?.(date.format("YYYY-MM-DD"));
237
+ };
238
+
239
+ const renderDays = () => {
240
+ const days = [];
241
+ const startDay = moment(startDate);
242
+
243
+ for (let i = 0; i < 7; i++) {
244
+ days.push(
245
+ <div key={i} className="text-center font-bold">
246
+ {startDay.add(i, "days").format("dd")}
247
+ </div>
248
+ );
249
+ }
250
+
251
+ return days;
252
+ };
253
+
254
+ const renderCells = () => {
255
+ const rows = [];
256
+ let days = [];
257
+ const day = moment(startDate);
258
+
259
+ while (day.isBefore(endDate) || day.isSame(endDate, "day")) {
260
+ for (let i = 0; i < 7; i++) {
261
+ const cloneDay = moment(day);
262
+
263
+ days.push(
264
+ <div
265
+ key={day.toString()}
266
+ className={`w-8 aspect-square flex items-center justify-center text-center rounded-lg transition-all
267
+ ${day.isSame(currentDate, "month") ? "text-foreground" : "text-light-foreground"}
268
+ ${day.isSame(selectedDate, "day") ? "bg-primary text-background" : "hover:bg-light-primary"}
269
+ ${day.isSame(moment(), "day") ? "border !border-primary" : "hover:bg-light-primary"}
270
+ ${(minDate && day.isBefore(moment(minDate))) || (maxDate && day.isAfter(moment(maxDate))) ? "opacity-10 cursor-not-allowed" : "cursor-pointer"}`}
271
+ onClick={() => handleDateClick(cloneDay)}
272
+ >{day.format("D")}</div>
273
+ );
274
+
275
+ day.add(1, "day");
276
+ }
277
+
278
+ rows.push(<div key={day.toString()} className="grid grid-cols-7 gap-1">{days}</div>);
279
+
280
+ days = [];
281
+ }
282
+
283
+ return rows;
284
+ };
285
+
286
+ const years = useMemo(() => {
287
+ const dumpYears = [];
288
+
289
+ for (let i = 1945; i <= moment().year(); i++) {
290
+ dumpYears.push(i);
291
+ }
292
+
293
+ return dumpYears;
294
+ }, []);
295
+
296
+ useEffect(() => {
297
+ if (activeYearRef.current && containerYearRef.current) {
298
+ containerYearRef.current.scrollTo({
299
+ top: activeYearRef.current.offsetTop - containerYearRef.current.offsetTop,
300
+ });
301
+ }
302
+ }, []);
303
+
304
+ return (
305
+ <div className="w-full flex gap-2 max-h-[260]">
306
+ <div
307
+ className="w-1/5 overflow-y-auto input-scroll pr-1"
308
+ ref={containerYearRef}
309
+ >
310
+ <div className="flex flex-col">
311
+ {years?.map((item) => {
312
+ const isActive = currentDate?.year() === item;
313
+
314
+ return (
315
+ <>
316
+ <div
317
+ key={item}
318
+ ref={isActive ? activeYearRef : null}
319
+ className={`py-1 px-2 font-semibold rounded-[6px] cursor-pointer ${isActive && "bg-light-primary"}`}
320
+ onClick={() => setCurrentDate(moment().set("year", item))}
321
+ >
322
+ {item}
323
+ </div>
324
+ </>
325
+ );
326
+ })}
327
+ </div>
328
+ </div>
329
+ <div className="w-4/5">
330
+ <div className="flex justify-between items-center mb-2">
331
+ <button
332
+ onClick={handlePrevMonth}
333
+ className="w-8 text-sm aspect-square rounded-full cursor-pointer"
334
+ >
335
+ <FontAwesomeIcon icon={faChevronLeft} />
336
+ </button>
337
+ <h2 className="font-semibold">{currentDate.format("MMMM")}</h2>
338
+ <button
339
+ onClick={handleNextMonth}
340
+ className="w-8 text-sm aspect-square rounded-full cursor-pointer"
341
+ >
342
+ <FontAwesomeIcon icon={faChevronRight} />
343
+ </button>
344
+ </div>
345
+ <div className="grid grid-cols-7 gap-1 mb-2">{renderDays()}</div>
346
+ <div>{renderCells()}</div>
347
+ </div>
348
+
349
+ {rightElement && <div>{rightElement}</div>}
350
+ </div>
351
+ );
352
+ };