@isma91/react-scheduler 4.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 (134) hide show
  1. package/.github/workflows/publish.yml +29 -0
  2. package/.github/workflows/tests.yml +35 -0
  3. package/.gitignore +32 -0
  4. package/.husky/pre-commit +2 -0
  5. package/.prettierignore +1 -0
  6. package/.prettierrc.json +7 -0
  7. package/.yarnrc.yml +1 -0
  8. package/LICENSE +24 -0
  9. package/README.md +172 -0
  10. package/dist/LICENSE +24 -0
  11. package/dist/README.md +172 -0
  12. package/dist/SchedulerComponent.d.ts +3 -0
  13. package/dist/components/common/Cell.d.ts +13 -0
  14. package/dist/components/common/LocaleArrow.d.ts +8 -0
  15. package/dist/components/common/ResourceHeader.d.ts +6 -0
  16. package/dist/components/common/Tabs.d.ts +16 -0
  17. package/dist/components/common/TodayTypo.d.ts +8 -0
  18. package/dist/components/common/WithResources.d.ts +6 -0
  19. package/dist/components/events/Actions.d.ts +8 -0
  20. package/dist/components/events/AgendaEventsList.d.ts +7 -0
  21. package/dist/components/events/CurrentTimeBar.d.ts +11 -0
  22. package/dist/components/events/EmptyAgenda.d.ts +2 -0
  23. package/dist/components/events/EventItem.d.ts +10 -0
  24. package/dist/components/events/EventItemPopover.d.ts +9 -0
  25. package/dist/components/events/MonthEvents.d.ts +13 -0
  26. package/dist/components/events/TodayEvents.d.ts +16 -0
  27. package/dist/components/hoc/DateProvider.d.ts +5 -0
  28. package/dist/components/inputs/DatePicker.d.ts +14 -0
  29. package/dist/components/inputs/Input.d.ts +19 -0
  30. package/dist/components/inputs/SelectInput.d.ts +22 -0
  31. package/dist/components/month/MonthTable.d.ts +8 -0
  32. package/dist/components/nav/DayDateBtn.d.ts +6 -0
  33. package/dist/components/nav/MonthDateBtn.d.ts +6 -0
  34. package/dist/components/nav/Navigation.d.ts +3 -0
  35. package/dist/components/nav/WeekDateBtn.d.ts +8 -0
  36. package/dist/components/week/WeekTable.d.ts +11 -0
  37. package/dist/helpers/constants.d.ts +4 -0
  38. package/dist/helpers/generals.d.ts +78 -0
  39. package/dist/hooks/useArrowDisable.d.ts +5 -0
  40. package/dist/hooks/useCellAttributes.d.ts +18 -0
  41. package/dist/hooks/useDragAttributes.d.ts +10 -0
  42. package/dist/hooks/useEventPermissions.d.ts +7 -0
  43. package/dist/hooks/useStore.d.ts +2 -0
  44. package/dist/hooks/useSyncScroll.d.ts +8 -0
  45. package/dist/hooks/useWindowResize.d.ts +4 -0
  46. package/dist/index.d.ts +3 -0
  47. package/dist/index.js +2853 -0
  48. package/dist/package.json +65 -0
  49. package/dist/positionManger/context.d.ts +14 -0
  50. package/dist/positionManger/provider.d.ts +5 -0
  51. package/dist/positionManger/usePosition.d.ts +4 -0
  52. package/dist/store/context.d.ts +2 -0
  53. package/dist/store/default.d.ts +245 -0
  54. package/dist/store/provider.d.ts +7 -0
  55. package/dist/store/types.d.ts +27 -0
  56. package/dist/styles/styles.d.ts +30 -0
  57. package/dist/types.d.ts +372 -0
  58. package/dist/views/Day.d.ts +2 -0
  59. package/dist/views/DayAgenda.d.ts +7 -0
  60. package/dist/views/Editor.d.ts +11 -0
  61. package/dist/views/Month.d.ts +2 -0
  62. package/dist/views/MonthAgenda.d.ts +7 -0
  63. package/dist/views/Week.d.ts +2 -0
  64. package/dist/views/WeekAgenda.d.ts +8 -0
  65. package/eslint.config.js +79 -0
  66. package/index.html +41 -0
  67. package/jest.config.ts +194 -0
  68. package/package.json +137 -0
  69. package/public/favicon.ico +0 -0
  70. package/public/logo192.png +0 -0
  71. package/public/logo512.png +0 -0
  72. package/public/manifest.json +25 -0
  73. package/public/robots.txt +3 -0
  74. package/scripts/post-pack.js +34 -0
  75. package/src/App.tsx +25 -0
  76. package/src/Page1.tsx +67 -0
  77. package/src/events.tsx +227 -0
  78. package/src/index.tsx +21 -0
  79. package/src/lib/SchedulerComponent.tsx +78 -0
  80. package/src/lib/__tests__/index.test.tsx +24 -0
  81. package/src/lib/components/common/Cell.tsx +52 -0
  82. package/src/lib/components/common/LocaleArrow.tsx +38 -0
  83. package/src/lib/components/common/ResourceHeader.tsx +73 -0
  84. package/src/lib/components/common/Tabs.tsx +119 -0
  85. package/src/lib/components/common/TodayTypo.tsx +44 -0
  86. package/src/lib/components/common/WithResources.tsx +98 -0
  87. package/src/lib/components/events/Actions.tsx +65 -0
  88. package/src/lib/components/events/AgendaEventsList.tsx +115 -0
  89. package/src/lib/components/events/CurrentTimeBar.tsx +59 -0
  90. package/src/lib/components/events/EmptyAgenda.tsx +27 -0
  91. package/src/lib/components/events/EventItem.tsx +180 -0
  92. package/src/lib/components/events/EventItemPopover.tsx +179 -0
  93. package/src/lib/components/events/MonthEvents.tsx +141 -0
  94. package/src/lib/components/events/TodayEvents.tsx +99 -0
  95. package/src/lib/components/hoc/DateProvider.tsx +19 -0
  96. package/src/lib/components/inputs/DatePicker.tsx +95 -0
  97. package/src/lib/components/inputs/Input.tsx +113 -0
  98. package/src/lib/components/inputs/SelectInput.tsx +164 -0
  99. package/src/lib/components/month/MonthTable.tsx +207 -0
  100. package/src/lib/components/nav/DayDateBtn.tsx +77 -0
  101. package/src/lib/components/nav/MonthDateBtn.tsx +80 -0
  102. package/src/lib/components/nav/Navigation.tsx +201 -0
  103. package/src/lib/components/nav/WeekDateBtn.tsx +89 -0
  104. package/src/lib/components/week/WeekTable.tsx +229 -0
  105. package/src/lib/helpers/constants.ts +4 -0
  106. package/src/lib/helpers/generals.tsx +354 -0
  107. package/src/lib/hooks/useArrowDisable.ts +26 -0
  108. package/src/lib/hooks/useCellAttributes.ts +67 -0
  109. package/src/lib/hooks/useDragAttributes.ts +31 -0
  110. package/src/lib/hooks/useEventPermissions.ts +42 -0
  111. package/src/lib/hooks/useStore.ts +8 -0
  112. package/src/lib/hooks/useSyncScroll.ts +31 -0
  113. package/src/lib/hooks/useWindowResize.ts +37 -0
  114. package/src/lib/index.tsx +14 -0
  115. package/src/lib/positionManger/context.ts +14 -0
  116. package/src/lib/positionManger/provider.tsx +113 -0
  117. package/src/lib/positionManger/usePosition.ts +8 -0
  118. package/src/lib/store/context.ts +5 -0
  119. package/src/lib/store/default.ts +157 -0
  120. package/src/lib/store/provider.tsx +211 -0
  121. package/src/lib/store/types.ts +33 -0
  122. package/src/lib/styles/styles.ts +256 -0
  123. package/src/lib/types.ts +423 -0
  124. package/src/lib/views/Day.tsx +265 -0
  125. package/src/lib/views/DayAgenda.tsx +57 -0
  126. package/src/lib/views/Editor.tsx +258 -0
  127. package/src/lib/views/Month.tsx +82 -0
  128. package/src/lib/views/MonthAgenda.tsx +84 -0
  129. package/src/lib/views/Week.tsx +92 -0
  130. package/src/lib/views/WeekAgenda.tsx +81 -0
  131. package/src/vite-env.d.ts +3 -0
  132. package/tsconfig.build.json +5 -0
  133. package/tsconfig.json +27 -0
  134. package/vite.config.js +40 -0
@@ -0,0 +1,179 @@
1
+ import { MouseEvent } from "react";
2
+ import { Box, IconButton, Popover, Typography, useTheme } from "@mui/material";
3
+ import useStore from "../../hooks/useStore";
4
+ import { ProcessedEvent } from "../../types";
5
+ import { PopperInner } from "../../styles/styles";
6
+ import EventActions from "./Actions";
7
+ import { differenceInDaysOmitTime, getHourFormat } from "../../helpers/generals";
8
+ import EventNoteRoundedIcon from "@mui/icons-material/EventNoteRounded";
9
+ import ClearRoundedIcon from "@mui/icons-material/ClearRounded";
10
+ import SupervisorAccountRoundedIcon from "@mui/icons-material/SupervisorAccountRounded";
11
+ import { format } from "date-fns";
12
+
13
+ type Props = {
14
+ event: ProcessedEvent;
15
+ anchorEl: Element | null;
16
+ onTriggerViewer: (el?: MouseEvent<Element>) => void;
17
+ };
18
+
19
+ const EventItemPopover = ({ anchorEl, event, onTriggerViewer }: Props) => {
20
+ const {
21
+ triggerDialog,
22
+ onDelete,
23
+ events,
24
+ handleState,
25
+ triggerLoading,
26
+ customViewer,
27
+ viewerExtraComponent,
28
+ fields,
29
+ resources,
30
+ resourceFields,
31
+ locale,
32
+ viewerTitleComponent,
33
+ viewerSubtitleComponent,
34
+ hourFormat,
35
+ translations,
36
+ onEventEdit,
37
+ } = useStore();
38
+ const theme = useTheme();
39
+ const hFormat = getHourFormat(hourFormat);
40
+ const displayStart = event._originalStart || event.start;
41
+ const displayEnd = event._originalEnd || event.end;
42
+ const hideDates = differenceInDaysOmitTime(displayStart, displayEnd) <= 0 && event.allDay;
43
+
44
+ const idKey = resourceFields.idField;
45
+ const hasResource = resources.filter((res) =>
46
+ Array.isArray(event[idKey]) ? event[idKey].includes(res[idKey]) : res[idKey] === event[idKey]
47
+ );
48
+
49
+ const handleDelete = async () => {
50
+ try {
51
+ triggerLoading(true);
52
+ let deletedId = event.event_id;
53
+ // Trigger custom/remote when provided
54
+ if (onDelete) {
55
+ const remoteId = await onDelete(deletedId);
56
+ if (remoteId) {
57
+ deletedId = remoteId;
58
+ } else {
59
+ deletedId = "";
60
+ }
61
+ }
62
+ if (deletedId) {
63
+ onTriggerViewer();
64
+ const updatedEvents = events.filter((e) => e.event_id !== deletedId);
65
+ handleState(updatedEvents, "events");
66
+ }
67
+ } catch (error) {
68
+ console.error(error);
69
+ } finally {
70
+ triggerLoading(false);
71
+ }
72
+ };
73
+
74
+ return (
75
+ <Popover
76
+ open={Boolean(anchorEl)}
77
+ anchorEl={anchorEl}
78
+ onClose={() => {
79
+ onTriggerViewer();
80
+ }}
81
+ anchorOrigin={{
82
+ vertical: "center",
83
+ horizontal: "center",
84
+ }}
85
+ transformOrigin={{
86
+ vertical: "top",
87
+ horizontal: "center",
88
+ }}
89
+ onClick={(e) => {
90
+ e.stopPropagation();
91
+ }}
92
+ >
93
+ {typeof customViewer === "function" ? (
94
+ customViewer(event, () => onTriggerViewer())
95
+ ) : (
96
+ <PopperInner>
97
+ <Box
98
+ sx={{
99
+ bgcolor: event.color || theme.palette.primary.main,
100
+ color: theme.palette.primary.contrastText,
101
+ }}
102
+ >
103
+ <div className="rs__popper_actions">
104
+ <div>
105
+ <IconButton
106
+ size="small"
107
+ onClick={() => {
108
+ onTriggerViewer();
109
+ }}
110
+ >
111
+ <ClearRoundedIcon color="disabled" />
112
+ </IconButton>
113
+ </div>
114
+ <EventActions
115
+ event={event}
116
+ onDelete={handleDelete}
117
+ onEdit={() => {
118
+ onTriggerViewer();
119
+ triggerDialog(true, event);
120
+
121
+ if (onEventEdit && typeof onEventEdit === "function") {
122
+ onEventEdit(event);
123
+ }
124
+ }}
125
+ />
126
+ </div>
127
+ {viewerTitleComponent instanceof Function ? (
128
+ viewerTitleComponent(event)
129
+ ) : (
130
+ <Typography style={{ padding: "5px 0" }} noWrap>
131
+ {event.title}
132
+ </Typography>
133
+ )}
134
+ </Box>
135
+ <div style={{ padding: "5px 10px" }}>
136
+ <Typography
137
+ style={{ display: "flex", alignItems: "center", gap: 8 }}
138
+ color="textSecondary"
139
+ variant="caption"
140
+ noWrap
141
+ >
142
+ <EventNoteRoundedIcon />
143
+ {hideDates
144
+ ? translations.event.allDay
145
+ : `${format(displayStart, `dd MMMM yyyy ${hFormat}`, {
146
+ locale: locale,
147
+ })} - ${format(displayEnd, `dd MMMM yyyy ${hFormat}`, {
148
+ locale: locale,
149
+ })}`}
150
+ </Typography>
151
+ {viewerSubtitleComponent instanceof Function ? (
152
+ viewerSubtitleComponent(event)
153
+ ) : (
154
+ <Typography variant="body2" style={{ padding: "5px 0" }}>
155
+ {event.subtitle}
156
+ </Typography>
157
+ )}
158
+ {hasResource.length > 0 && (
159
+ <Typography
160
+ style={{ display: "flex", alignItems: "center", gap: 8 }}
161
+ color="textSecondary"
162
+ variant="caption"
163
+ noWrap
164
+ >
165
+ <SupervisorAccountRoundedIcon />
166
+ {hasResource.map((res) => res[resourceFields.textField]).join(", ")}
167
+ </Typography>
168
+ )}
169
+ {viewerExtraComponent instanceof Function
170
+ ? viewerExtraComponent(fields, event)
171
+ : viewerExtraComponent}
172
+ </div>
173
+ </PopperInner>
174
+ )}
175
+ </Popover>
176
+ );
177
+ };
178
+
179
+ export default EventItemPopover;
@@ -0,0 +1,141 @@
1
+ import { Fragment, useMemo } from "react";
2
+ import {
3
+ closestTo,
4
+ isBefore,
5
+ startOfWeek,
6
+ differenceInDays,
7
+ differenceInCalendarWeeks,
8
+ format,
9
+ } from "date-fns";
10
+ import { ProcessedEvent } from "../../types";
11
+ import { Typography } from "@mui/material";
12
+ import EventItem from "./EventItem";
13
+ import {
14
+ MONTH_BAR_HEIGHT,
15
+ MONTH_NUMBER_HEIGHT,
16
+ MULTI_DAY_EVENT_HEIGHT,
17
+ } from "../../helpers/constants";
18
+ import { convertEventTimeZone, differenceInDaysOmitTime } from "../../helpers/generals";
19
+ import useStore from "../../hooks/useStore";
20
+ import usePosition from "../../positionManger/usePosition";
21
+
22
+ interface MonthEventProps {
23
+ events: ProcessedEvent[];
24
+ resourceId?: string;
25
+ today: Date;
26
+ eachWeekStart: Date[];
27
+ eachFirstDayInCalcRow: Date | null;
28
+ daysList: Date[];
29
+ onViewMore(day: Date): void;
30
+ cellHeight: number;
31
+ }
32
+
33
+ const MonthEvents = ({
34
+ events,
35
+ resourceId,
36
+ today,
37
+ eachWeekStart,
38
+ eachFirstDayInCalcRow,
39
+ daysList,
40
+ onViewMore,
41
+ cellHeight,
42
+ }: MonthEventProps) => {
43
+ const LIMIT = Math.round((cellHeight - MONTH_NUMBER_HEIGHT) / MULTI_DAY_EVENT_HEIGHT - 1);
44
+ const { translations, month, locale, timeZone } = useStore();
45
+ const { renderedSlots } = usePosition();
46
+
47
+ const renderEvents = useMemo(() => {
48
+ const elements: React.ReactNode[] = [];
49
+
50
+ for (let i = 0; i < Math.min(events.length, LIMIT + 1); i++) {
51
+ const event = convertEventTimeZone(events[i], timeZone);
52
+ const fromPrevWeek = !!eachFirstDayInCalcRow && isBefore(event.start, eachFirstDayInCalcRow);
53
+ const start = fromPrevWeek && eachFirstDayInCalcRow ? eachFirstDayInCalcRow : event.start;
54
+ let eventLength = differenceInDaysOmitTime(start, event.end) + 1;
55
+
56
+ const toNextWeek =
57
+ differenceInCalendarWeeks(event.end, start, {
58
+ weekStartsOn: month?.weekStartOn,
59
+ locale,
60
+ }) > 0;
61
+
62
+ if (toNextWeek) {
63
+ // Rethink it
64
+ const NotAccurateWeekStart = startOfWeek(event.start, {
65
+ weekStartsOn: month?.weekStartOn,
66
+ locale,
67
+ });
68
+ const closestStart = closestTo(NotAccurateWeekStart, eachWeekStart);
69
+ if (closestStart) {
70
+ eventLength =
71
+ daysList.length -
72
+ (!eachFirstDayInCalcRow ? differenceInDays(event.start, closestStart) : 0);
73
+ }
74
+ }
75
+
76
+ const day = format(today, "yyyy-MM-dd");
77
+ const rendered = renderedSlots?.[resourceId || "all"]?.[day];
78
+ const position = rendered?.[event.event_id] || 0;
79
+
80
+ const topSpace = Math.min(position, LIMIT) * MULTI_DAY_EVENT_HEIGHT + MONTH_NUMBER_HEIGHT;
81
+
82
+ if (position >= LIMIT) {
83
+ elements.push(
84
+ <Typography
85
+ key={i}
86
+ width="100%"
87
+ className="rs__multi_day rs__hover__op"
88
+ style={{ top: topSpace, fontSize: 11 }}
89
+ onClick={(e) => {
90
+ e.stopPropagation();
91
+ onViewMore(today);
92
+ }}
93
+ >
94
+ {`${Math.abs(events.length - i)} ${translations.moreEvents}`}
95
+ </Typography>
96
+ );
97
+ break;
98
+ }
99
+
100
+ elements.push(
101
+ <div
102
+ key={`${event.event_id}_${i}`}
103
+ className="rs__multi_day"
104
+ style={{
105
+ top: topSpace,
106
+ width: `${100 * eventLength}%`,
107
+ height: MONTH_BAR_HEIGHT,
108
+ }}
109
+ >
110
+ <EventItem
111
+ event={event}
112
+ showdate={false}
113
+ multiday={differenceInDaysOmitTime(event.start, event.end) > 0}
114
+ hasPrev={fromPrevWeek}
115
+ hasNext={toNextWeek}
116
+ />
117
+ </div>
118
+ );
119
+ }
120
+
121
+ return elements;
122
+ }, [
123
+ resourceId,
124
+ renderedSlots,
125
+ events,
126
+ LIMIT,
127
+ eachFirstDayInCalcRow,
128
+ month?.weekStartOn,
129
+ locale,
130
+ today,
131
+ eachWeekStart,
132
+ daysList.length,
133
+ translations.moreEvents,
134
+ onViewMore,
135
+ timeZone,
136
+ ]);
137
+
138
+ return <Fragment>{renderEvents}</Fragment>;
139
+ };
140
+
141
+ export default MonthEvents;
@@ -0,0 +1,99 @@
1
+ import { differenceInMinutes } from "date-fns";
2
+ import { Fragment } from "react";
3
+ import { BORDER_HEIGHT } from "../../helpers/constants";
4
+ import { isTimeZonedToday, traversCrossingEvents } from "../../helpers/generals";
5
+ import { ProcessedEvent } from "../../types";
6
+ import CurrentTimeBar from "./CurrentTimeBar";
7
+ import EventItem from "./EventItem";
8
+
9
+ interface TodayEventsProps {
10
+ todayEvents: ProcessedEvent[];
11
+ today: Date;
12
+ startHour: number;
13
+ endHour: number;
14
+ step: number;
15
+ minuteHeight: number;
16
+ direction: "rtl" | "ltr";
17
+ timeZone?: string;
18
+ currentTime?: Date;
19
+ showCurrentTimeBar?: boolean;
20
+ currentTimeBarColor?: string;
21
+ }
22
+ const TodayEvents = ({
23
+ todayEvents,
24
+ today,
25
+ startHour,
26
+ endHour,
27
+ step,
28
+ minuteHeight,
29
+ direction,
30
+ timeZone,
31
+ currentTime,
32
+ showCurrentTimeBar = true,
33
+ currentTimeBarColor,
34
+ }: TodayEventsProps) => {
35
+ const crossingIds: Array<number | string> = [];
36
+
37
+ return (
38
+ <Fragment>
39
+ {showCurrentTimeBar && isTimeZonedToday({ dateLeft: today, timeZone }) && (
40
+ <CurrentTimeBar
41
+ startHour={startHour}
42
+ step={step}
43
+ minuteHeight={minuteHeight}
44
+ timeZone={timeZone}
45
+ zIndex={2 * todayEvents.length + 1}
46
+ currentTime={currentTime}
47
+ color={currentTimeBarColor}
48
+ />
49
+ )}
50
+
51
+ {todayEvents.map((event, i) => {
52
+ const maxHeight = (endHour * 60 - startHour * 60) * minuteHeight;
53
+ const eventHeight = differenceInMinutes(event.end, event.start) * minuteHeight;
54
+ const height = Math.min(eventHeight, maxHeight) - BORDER_HEIGHT;
55
+
56
+ const calendarStartInMins = startHour * 60;
57
+ const eventStartInMins = event.start.getHours() * 60 + event.start.getMinutes();
58
+ const minituesFromTop = Math.max(eventStartInMins - calendarStartInMins, 0);
59
+
60
+ const topSpace = minituesFromTop * minuteHeight;
61
+ /** Add border factor to height of each slot */
62
+ const slots = height / 60;
63
+ const heightBorderFactor = slots * BORDER_HEIGHT;
64
+
65
+ /** Calculate top space */
66
+ const slotsFromTop = minituesFromTop / step;
67
+ const top = topSpace + slotsFromTop;
68
+
69
+ const crossingEvents = traversCrossingEvents(todayEvents, event);
70
+ const alreadyRendered = crossingEvents.filter((e) => crossingIds.includes(e.event_id));
71
+ crossingIds.push(event.event_id);
72
+
73
+ return (
74
+ <div
75
+ key={`${event.event_id}/${event.recurrenceId || ""}`}
76
+ className="rs__event__item"
77
+ style={{
78
+ height: height + heightBorderFactor,
79
+ top,
80
+ width:
81
+ alreadyRendered.length > 0
82
+ ? `calc(100% - ${100 - 98 / (alreadyRendered.length + 1)}%)`
83
+ : "98%", // Leave some space to click cell
84
+ zIndex: todayEvents.length + i,
85
+ [direction === "rtl" ? "right" : "left"]:
86
+ alreadyRendered.length > 0
87
+ ? `${(100 / (crossingEvents.length + 1)) * alreadyRendered.length}%`
88
+ : "",
89
+ }}
90
+ >
91
+ <EventItem event={event} />
92
+ </div>
93
+ );
94
+ })}
95
+ </Fragment>
96
+ );
97
+ };
98
+
99
+ export default TodayEvents;
@@ -0,0 +1,19 @@
1
+ import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider";
2
+ import useStore from "../../hooks/useStore";
3
+ import { AdapterDateFns } from "@mui/x-date-pickers/AdapterDateFns";
4
+
5
+ interface AuxProps {
6
+ children: React.ReactNode;
7
+ }
8
+
9
+ const DateProvider = ({ children }: AuxProps) => {
10
+ const { locale } = useStore();
11
+
12
+ return (
13
+ <LocalizationProvider dateAdapter={AdapterDateFns} adapterLocale={locale}>
14
+ {children}
15
+ </LocalizationProvider>
16
+ );
17
+ };
18
+
19
+ export default DateProvider;
@@ -0,0 +1,95 @@
1
+ import { useCallback, useEffect, useState } from "react";
2
+ import DateProvider from "../hoc/DateProvider";
3
+ import { DatePicker } from "@mui/x-date-pickers/DatePicker";
4
+ import { DateTimePicker } from "@mui/x-date-pickers/DateTimePicker";
5
+ import useStore from "../../hooks/useStore";
6
+
7
+ interface EditorDatePickerProps {
8
+ type?: "date" | "datetime";
9
+ label?: string;
10
+ variant?: "standard" | "filled" | "outlined";
11
+ value: Date | string;
12
+ name: string;
13
+ onChange(name: string, date: Date): void;
14
+ error?: boolean;
15
+ errMsg?: string;
16
+ touched?: boolean;
17
+ required?: boolean;
18
+ }
19
+
20
+ const EditorDatePicker = ({
21
+ type = "datetime",
22
+ value,
23
+ label,
24
+ name,
25
+ onChange,
26
+ variant = "outlined",
27
+ error,
28
+ errMsg,
29
+ touched,
30
+ required,
31
+ }: EditorDatePickerProps) => {
32
+ const { translations } = useStore();
33
+ const [state, setState] = useState({
34
+ touched: false,
35
+ valid: !!value,
36
+ errorMsg: errMsg
37
+ ? errMsg
38
+ : required
39
+ ? translations?.validation?.required || "Required"
40
+ : undefined,
41
+ });
42
+
43
+ const Picker = type === "date" ? DatePicker : DateTimePicker;
44
+
45
+ const hasError = state.touched && (error || !state.valid);
46
+
47
+ const handleChange = useCallback(
48
+ (value: string | Date) => {
49
+ const isValidDate = !isNaN(Date.parse(value as string));
50
+ const val = typeof value === "string" && isValidDate ? new Date(value) : value;
51
+ let isValid = true;
52
+ let errorMsg = errMsg;
53
+ if (required && !val) {
54
+ isValid = false;
55
+ errorMsg = errMsg || translations?.validation?.required || "Required";
56
+ }
57
+
58
+ setState((prev) => {
59
+ return { ...prev, touched: true, valid: isValid, errorMsg: errorMsg };
60
+ });
61
+
62
+ onChange(name, val as Date);
63
+ },
64
+ [errMsg, name, onChange, required, translations?.validation?.required]
65
+ );
66
+
67
+ useEffect(() => {
68
+ if (touched) {
69
+ handleChange(value);
70
+ }
71
+ }, [handleChange, touched, value]);
72
+
73
+ return (
74
+ <DateProvider>
75
+ <Picker
76
+ value={value instanceof Date ? value : new Date(value)}
77
+ label={label}
78
+ onChange={(e) => {
79
+ handleChange(e as Date);
80
+ }}
81
+ minutesStep={5}
82
+ slotProps={{
83
+ textField: {
84
+ variant,
85
+ helperText: hasError && state.errorMsg,
86
+ error: hasError,
87
+ fullWidth: true,
88
+ },
89
+ }}
90
+ />
91
+ </DateProvider>
92
+ );
93
+ };
94
+
95
+ export { EditorDatePicker };
@@ -0,0 +1,113 @@
1
+ import { useState, useEffect, useCallback } from "react";
2
+ import { TextField, Typography } from "@mui/material";
3
+ import useStore from "../../hooks/useStore";
4
+
5
+ interface EditorInputProps {
6
+ variant?: "standard" | "filled" | "outlined";
7
+ label?: string;
8
+ placeholder?: string;
9
+ required?: boolean;
10
+ min?: number;
11
+ max?: number;
12
+ email?: boolean;
13
+ decimal?: boolean;
14
+ disabled?: boolean;
15
+ multiline?: boolean;
16
+ rows?: number;
17
+ value: string;
18
+ name: string;
19
+ onChange(name: string, value: string, isValid: boolean): void;
20
+ touched?: boolean;
21
+ }
22
+
23
+ const EditorInput = ({
24
+ variant = "outlined",
25
+ label,
26
+ placeholder,
27
+ value,
28
+ name,
29
+ required,
30
+ min,
31
+ max,
32
+ email,
33
+ decimal,
34
+ onChange,
35
+ disabled,
36
+ multiline,
37
+ rows,
38
+ touched,
39
+ }: EditorInputProps) => {
40
+ const [state, setState] = useState({
41
+ touched: false,
42
+ valid: false,
43
+ errorMsg: "",
44
+ });
45
+ const { translations } = useStore();
46
+
47
+ const handleChange = useCallback(
48
+ (value: string) => {
49
+ const val = value;
50
+ let isValid = true;
51
+ let errorMsg = "";
52
+ if (email) {
53
+ const reg =
54
+ /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
55
+ isValid = reg.test(val) && isValid;
56
+ errorMsg = translations?.validation?.invalidEmail || "Invalid Email";
57
+ }
58
+ if (decimal) {
59
+ const reg = /^[0-9]+(\.[0-9]*)?$/;
60
+ isValid = reg.test(val) && isValid;
61
+ errorMsg = translations?.validation?.onlyNumbers || "Only Numbers Allowed";
62
+ }
63
+ if (min && `${val}`.trim().length < min) {
64
+ isValid = false;
65
+ errorMsg =
66
+ typeof translations?.validation?.min === "function"
67
+ ? translations?.validation?.min(min)
68
+ : translations?.validation?.min || `Minimum ${min} letters`;
69
+ }
70
+ if (max && `${val}`.trim().length > max) {
71
+ isValid = false;
72
+ errorMsg =
73
+ typeof translations?.validation?.max === "function"
74
+ ? translations?.validation?.max(max)
75
+ : translations?.validation?.max || `Maximum ${max} letters`;
76
+ }
77
+ if (required && `${val}`.trim().length <= 0) {
78
+ isValid = false;
79
+ errorMsg = translations?.validation?.required || "Required";
80
+ }
81
+ setState({ touched: true, valid: isValid, errorMsg: errorMsg });
82
+ onChange(name, val, isValid);
83
+ },
84
+ [decimal, email, max, min, name, onChange, required, translations?.validation]
85
+ );
86
+
87
+ useEffect(() => {
88
+ if (touched) {
89
+ handleChange(value);
90
+ }
91
+ }, [handleChange, touched, value]);
92
+
93
+ return (
94
+ <TextField
95
+ variant={variant}
96
+ label={label && <Typography variant="body2">{`${label} ${required ? "*" : ""}`}</Typography>}
97
+ value={value}
98
+ name={name}
99
+ onChange={(e) => handleChange(e.target.value)}
100
+ disabled={disabled}
101
+ error={state.touched && !state.valid}
102
+ helperText={state.touched && !state.valid && state.errorMsg}
103
+ multiline={multiline}
104
+ rows={rows}
105
+ style={{ width: "100%" }}
106
+ InputProps={{
107
+ placeholder: placeholder || "",
108
+ }}
109
+ />
110
+ );
111
+ };
112
+
113
+ export { EditorInput };