sanity-plugin-recurring-dates 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 (37) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +186 -0
  3. package/dist/index.d.ts +29 -0
  4. package/dist/index.esm.js +9173 -0
  5. package/dist/index.esm.js.map +1 -0
  6. package/dist/index.js +9197 -0
  7. package/dist/index.js.map +1 -0
  8. package/package.json +93 -0
  9. package/sanity.json +8 -0
  10. package/src/components/CustomRule/CustomRule.tsx +230 -0
  11. package/src/components/CustomRule/Monthly.tsx +67 -0
  12. package/src/components/CustomRule/Weekly.tsx +62 -0
  13. package/src/components/CustomRule/index.tsx +1 -0
  14. package/src/components/DateInputs/CommonDateTimeInput.tsx +111 -0
  15. package/src/components/DateInputs/DateInput.tsx +81 -0
  16. package/src/components/DateInputs/DateTimeInput.tsx +122 -0
  17. package/src/components/DateInputs/base/DatePicker.tsx +34 -0
  18. package/src/components/DateInputs/base/DateTimeInput.tsx +104 -0
  19. package/src/components/DateInputs/base/LazyTextInput.tsx +77 -0
  20. package/src/components/DateInputs/base/calendar/Calendar.tsx +381 -0
  21. package/src/components/DateInputs/base/calendar/CalendarDay.tsx +47 -0
  22. package/src/components/DateInputs/base/calendar/CalendarMonth.tsx +52 -0
  23. package/src/components/DateInputs/base/calendar/YearInput.tsx +22 -0
  24. package/src/components/DateInputs/base/calendar/constants.ts +33 -0
  25. package/src/components/DateInputs/base/calendar/features.ts +4 -0
  26. package/src/components/DateInputs/base/calendar/utils.ts +34 -0
  27. package/src/components/DateInputs/index.ts +2 -0
  28. package/src/components/DateInputs/types.ts +4 -0
  29. package/src/components/DateInputs/utils.ts +4 -0
  30. package/src/components/RecurringDate.tsx +139 -0
  31. package/src/constants.ts +36 -0
  32. package/src/index.ts +2 -0
  33. package/src/plugin.tsx +19 -0
  34. package/src/schema/recurringDates.tsx +44 -0
  35. package/src/types.ts +27 -0
  36. package/src/utils.ts +16 -0
  37. package/v2-incompatible.js +11 -0
@@ -0,0 +1,381 @@
1
+ import {Box, Button, Flex, Grid, Select, Text, useForwardedRef} from '@sanity/ui'
2
+ import {ChevronLeftIcon, ChevronRightIcon} from '@sanity/icons'
3
+ import {addDays, addMonths, setDate, setHours, setMinutes, setMonth, setYear} from 'date-fns'
4
+ import {range} from 'lodash'
5
+ import React, {forwardRef, useCallback, useEffect} from 'react'
6
+ import {CalendarMonth} from './CalendarMonth'
7
+ import {ARROW_KEYS, HOURS_24, MONTH_NAMES, DEFAULT_TIME_PRESETS} from './constants'
8
+ import {features} from './features'
9
+ import {formatTime} from './utils'
10
+ import {YearInput} from './YearInput'
11
+
12
+ type CalendarProps = Omit<React.ComponentProps<'div'>, 'onSelect'> & {
13
+ selectTime?: boolean
14
+ selectedDate?: Date
15
+ timeStep?: number
16
+ onSelect: (date: Date) => void
17
+ focusedDate: Date
18
+ onFocusedDateChange: (index: Date) => void
19
+ }
20
+
21
+ // This is used to maintain focus on a child element of the calendar-grid between re-renders
22
+ // When using arrow keys to move focus from a day in one month to another we are setting focus at the button for the day
23
+ // after it has changed but *only* if we *already* had focus inside the calendar grid (e.g not if focus was on the "next
24
+ // year" button, or any of the other controls)
25
+ // When moving from the last day of a month that displays 6 weeks in the grid to a month that displays 5 weeks, current
26
+ // focus gets lost on render, so this provides us with a stable element to help us preserve focus on a child element of
27
+ // the calendar grid between re-renders
28
+ const PRESERVE_FOCUS_ELEMENT = (
29
+ <span
30
+ data-preserve-focus
31
+ style={{overflow: 'hidden', position: 'absolute', outline: 'none'}}
32
+ tabIndex={-1}
33
+ />
34
+ )
35
+
36
+ export const Calendar = forwardRef(function Calendar(
37
+ props: CalendarProps,
38
+ forwardedRef: React.ForwardedRef<HTMLDivElement>
39
+ ) {
40
+ const {
41
+ selectTime,
42
+ onFocusedDateChange,
43
+ selectedDate = new Date(),
44
+ focusedDate = selectedDate,
45
+ timeStep = 1,
46
+ onSelect,
47
+ ...restProps
48
+ } = props
49
+
50
+ const setFocusedDate = useCallback(
51
+ (date: Date) => onFocusedDateChange(date),
52
+ [onFocusedDateChange]
53
+ )
54
+
55
+ const setFocusedDateMonth = useCallback(
56
+ (month: number) => setFocusedDate(setDate(setMonth(focusedDate, month), 1)),
57
+ [focusedDate, setFocusedDate]
58
+ )
59
+
60
+ const handleFocusedMonthChange = useCallback(
61
+ (e: React.FormEvent<HTMLSelectElement>) => setFocusedDateMonth(Number(e.currentTarget.value)),
62
+ [setFocusedDateMonth]
63
+ )
64
+
65
+ const moveFocusedDate = useCallback(
66
+ (by: number) => setFocusedDate(addMonths(focusedDate, by)),
67
+ [focusedDate, setFocusedDate]
68
+ )
69
+
70
+ const setFocusedDateYear = useCallback(
71
+ (year: number) => setFocusedDate(setYear(focusedDate, year)),
72
+ [focusedDate, setFocusedDate]
73
+ )
74
+
75
+ const handleDateChange = useCallback(
76
+ (date: Date) => {
77
+ onSelect(setMinutes(setHours(date, selectedDate.getHours()), selectedDate.getMinutes()))
78
+ },
79
+ [onSelect, selectedDate]
80
+ )
81
+
82
+ const handleMinutesChange = useCallback(
83
+ (event: React.FormEvent<HTMLSelectElement>) => {
84
+ const m = Number(event.currentTarget.value)
85
+ onSelect(setMinutes(selectedDate, m))
86
+ },
87
+ [onSelect, selectedDate]
88
+ )
89
+
90
+ const handleHoursChange = useCallback(
91
+ (event: React.FormEvent<HTMLSelectElement>) => {
92
+ const m = Number(event.currentTarget.value)
93
+ onSelect(setHours(selectedDate, m))
94
+ },
95
+ [onSelect, selectedDate]
96
+ )
97
+
98
+ const handleTimeChange = useCallback(
99
+ (hours: number, mins: number) => {
100
+ onSelect(setHours(setMinutes(selectedDate, mins), hours))
101
+ },
102
+ [onSelect, selectedDate]
103
+ )
104
+
105
+ const ref = useForwardedRef(forwardedRef)
106
+
107
+ const focusCurrentWeekDay = useCallback(() => {
108
+ ref.current?.querySelector<HTMLElement>(`[data-focused="true"]`)?.focus()
109
+ }, [ref])
110
+
111
+ const handleKeyDown = useCallback(
112
+ (event: any) => {
113
+ if (!ARROW_KEYS.includes(event.key)) {
114
+ return
115
+ }
116
+ event.preventDefault()
117
+ if (event.target.hasAttribute('data-calendar-grid')) {
118
+ focusCurrentWeekDay()
119
+ return
120
+ }
121
+ if (event.key === 'ArrowUp') {
122
+ onFocusedDateChange(addDays(focusedDate, -7))
123
+ }
124
+ if (event.key === 'ArrowDown') {
125
+ onFocusedDateChange(addDays(focusedDate, 7))
126
+ }
127
+ if (event.key === 'ArrowLeft') {
128
+ onFocusedDateChange(addDays(focusedDate, -1))
129
+ }
130
+ if (event.key === 'ArrowRight') {
131
+ onFocusedDateChange(addDays(focusedDate, 1))
132
+ }
133
+ // set focus temporarily on this element to make sure focus is still inside the calendar-grid after re-render
134
+ ref.current?.querySelector<HTMLElement>('[data-preserve-focus]')?.focus()
135
+ },
136
+ [ref, focusCurrentWeekDay, onFocusedDateChange, focusedDate]
137
+ )
138
+
139
+ useEffect(() => {
140
+ focusCurrentWeekDay()
141
+ }, [focusCurrentWeekDay])
142
+
143
+ useEffect(() => {
144
+ const currentFocusInCalendarGrid = document.activeElement?.matches(
145
+ '[data-calendar-grid], [data-calendar-grid] [data-preserve-focus]'
146
+ )
147
+ if (
148
+ // Only move focus if it's currently in the calendar grid
149
+ currentFocusInCalendarGrid
150
+ ) {
151
+ focusCurrentWeekDay()
152
+ }
153
+ }, [ref, focusCurrentWeekDay, focusedDate])
154
+
155
+ const handleYesterdayClick = useCallback(
156
+ () => handleDateChange(addDays(new Date(), -1)),
157
+ [handleDateChange]
158
+ )
159
+
160
+ const handleTodayClick = useCallback(() => handleDateChange(new Date()), [handleDateChange])
161
+
162
+ const handleTomorrowClick = useCallback(
163
+ () => handleDateChange(addDays(new Date(), 1)),
164
+ [handleDateChange]
165
+ )
166
+
167
+ const handleNowClick = useCallback(() => onSelect(new Date()), [onSelect])
168
+
169
+ return (
170
+ <Box data-ui="Calendar" {...restProps} ref={ref}>
171
+ {/* Select date */}
172
+ <Box padding={2}>
173
+ {/* Day presets */}
174
+ {features.dayPresets && (
175
+ <Grid columns={3} data-ui="CalendaryDayPresets" gap={1}>
176
+ <Button text="Yesterday" mode="bleed" fontSize={1} onClick={handleYesterdayClick} />
177
+ <Button text="Today" mode="bleed" fontSize={1} onClick={handleTodayClick} />
178
+ <Button text="Tomorrow" mode="bleed" fontSize={1} onClick={handleTomorrowClick} />
179
+ </Grid>
180
+ )}
181
+
182
+ {/* Select month and year */}
183
+ <Flex>
184
+ <Box flex={1}>
185
+ <CalendarMonthSelect
186
+ moveFocusedDate={moveFocusedDate}
187
+ onChange={handleFocusedMonthChange}
188
+ value={focusedDate?.getMonth()}
189
+ />
190
+ </Box>
191
+ <Box marginLeft={2}>
192
+ <CalendarYearSelect
193
+ moveFocusedDate={moveFocusedDate}
194
+ onChange={setFocusedDateYear}
195
+ value={focusedDate.getFullYear()}
196
+ />
197
+ </Box>
198
+ </Flex>
199
+
200
+ {/* Selected month (grid of days) */}
201
+ <Box
202
+ data-calendar-grid
203
+ onKeyDown={handleKeyDown}
204
+ marginTop={2}
205
+ overflow="hidden"
206
+ tabIndex={0}
207
+ >
208
+ <CalendarMonth
209
+ date={focusedDate}
210
+ focused={focusedDate}
211
+ onSelect={handleDateChange}
212
+ selected={selectedDate}
213
+ />
214
+ {PRESERVE_FOCUS_ELEMENT}
215
+ </Box>
216
+ </Box>
217
+
218
+ {/* Select time */}
219
+ {selectTime && (
220
+ <Box padding={2} style={{borderTop: '1px solid var(--card-border-color)'}}>
221
+ <Flex align="center">
222
+ <Flex align="center" flex={1}>
223
+ <Box>
224
+ <Select
225
+ aria-label="Select hour"
226
+ value={selectedDate?.getHours()}
227
+ onChange={handleHoursChange}
228
+ >
229
+ {HOURS_24.map((h) => (
230
+ <option key={h} value={h}>
231
+ {`${h}`.padStart(2, '0')}
232
+ </option>
233
+ ))}
234
+ </Select>
235
+ </Box>
236
+
237
+ <Box paddingX={1}>
238
+ <Text>:</Text>
239
+ </Box>
240
+
241
+ <Box>
242
+ <Select
243
+ aria-label="Select minutes"
244
+ value={selectedDate?.getMinutes()}
245
+ onChange={handleMinutesChange}
246
+ >
247
+ {range(0, 60, timeStep).map((m) => (
248
+ <option key={m} value={m}>
249
+ {`${m}`.padStart(2, '0')}
250
+ </option>
251
+ ))}
252
+ </Select>
253
+ </Box>
254
+ </Flex>
255
+
256
+ <Box marginLeft={2}>
257
+ <Button text="Set to current time" mode="bleed" onClick={handleNowClick} />
258
+ </Box>
259
+ </Flex>
260
+
261
+ {features.timePresets && (
262
+ <Flex direction="row" justify="center" align="center" style={{marginTop: 5}}>
263
+ {DEFAULT_TIME_PRESETS.map(([hours, minutes]) => {
264
+ return (
265
+ <CalendarTimePresetButton
266
+ key={`${hours}-${minutes}`}
267
+ hours={hours}
268
+ minutes={minutes}
269
+ onTimeChange={handleTimeChange}
270
+ selectedDate={selectedDate}
271
+ />
272
+ )
273
+ })}
274
+ </Flex>
275
+ )}
276
+ </Box>
277
+ )}
278
+ </Box>
279
+ )
280
+ })
281
+
282
+ function CalendarTimePresetButton(props: {
283
+ hours: number
284
+ minutes: number
285
+ onTimeChange: (hours: number, minutes: number) => void
286
+ selectedDate: Date
287
+ }) {
288
+ const {hours, minutes, onTimeChange, selectedDate} = props
289
+ const formatted = formatTime(hours, minutes)
290
+
291
+ const handleClick = useCallback(() => {
292
+ onTimeChange(hours, minutes)
293
+ }, [hours, minutes, onTimeChange])
294
+
295
+ return (
296
+ <Button
297
+ text={formatted}
298
+ aria-label={`${formatted} on ${selectedDate.toDateString()}`}
299
+ mode="bleed"
300
+ fontSize={1}
301
+ onClick={handleClick}
302
+ />
303
+ )
304
+ }
305
+
306
+ function CalendarMonthSelect(props: {
307
+ moveFocusedDate: (by: number) => void
308
+ onChange: (e: React.FormEvent<HTMLSelectElement>) => void
309
+ value?: number
310
+ }) {
311
+ const {moveFocusedDate, onChange, value} = props
312
+
313
+ const handlePrevMonthClick = useCallback(() => moveFocusedDate(-1), [moveFocusedDate])
314
+
315
+ const handleNextMonthClick = useCallback(() => moveFocusedDate(1), [moveFocusedDate])
316
+
317
+ return (
318
+ <Flex flex={1}>
319
+ <Button
320
+ aria-label="Go to previous month"
321
+ onClick={handlePrevMonthClick}
322
+ mode="bleed"
323
+ icon={ChevronLeftIcon}
324
+ paddingX={2}
325
+ radius={0}
326
+ />
327
+ <Box flex={1}>
328
+ <Select radius={0} value={value} onChange={onChange}>
329
+ {MONTH_NAMES.map((m, i) => (
330
+ // eslint-disable-next-line react/no-array-index-key
331
+ <option key={i} value={i}>
332
+ {m}
333
+ </option>
334
+ ))}
335
+ </Select>
336
+ </Box>
337
+ <Button
338
+ aria-label="Go to next month"
339
+ mode="bleed"
340
+ icon={ChevronRightIcon}
341
+ onClick={handleNextMonthClick}
342
+ paddingX={2}
343
+ radius={0}
344
+ />
345
+ </Flex>
346
+ )
347
+ }
348
+
349
+ function CalendarYearSelect(props: {
350
+ moveFocusedDate: (by: number) => void
351
+ onChange: (year: number) => void
352
+ value?: number
353
+ }) {
354
+ const {moveFocusedDate, onChange, value} = props
355
+
356
+ const handlePrevYearClick = useCallback(() => moveFocusedDate(-12), [moveFocusedDate])
357
+
358
+ const handleNextYearClick = useCallback(() => moveFocusedDate(12), [moveFocusedDate])
359
+
360
+ return (
361
+ <Flex>
362
+ <Button
363
+ aria-label="Previous year"
364
+ onClick={handlePrevYearClick}
365
+ mode="bleed"
366
+ icon={ChevronLeftIcon}
367
+ paddingX={2}
368
+ radius={0}
369
+ />
370
+ <YearInput value={value} onChange={onChange} radius={0} style={{width: 65}} />
371
+ <Button
372
+ aria-label="Next year"
373
+ onClick={handleNextYearClick}
374
+ mode="bleed"
375
+ icon={ChevronRightIcon}
376
+ paddingX={2}
377
+ radius={0}
378
+ />
379
+ </Flex>
380
+ )
381
+ }
@@ -0,0 +1,47 @@
1
+ import {Card, Text} from '@sanity/ui'
2
+ import React, {useCallback} from 'react'
3
+
4
+ interface CalendarDayProps {
5
+ date: Date
6
+ focused?: boolean
7
+ onSelect: (date: Date) => void
8
+ isCurrentMonth?: boolean
9
+ isToday: boolean
10
+ selected?: boolean
11
+ }
12
+
13
+ export function CalendarDay(props: CalendarDayProps) {
14
+ const {date, focused, isCurrentMonth, isToday, onSelect, selected} = props
15
+
16
+ const handleClick = useCallback(() => {
17
+ onSelect(date)
18
+ }, [date, onSelect])
19
+
20
+ return (
21
+ <div aria-selected={selected} data-ui="CalendarDay">
22
+ <Card
23
+ aria-label={date.toDateString()}
24
+ aria-pressed={selected}
25
+ as="button"
26
+ __unstable_focusRing
27
+ data-weekday
28
+ data-focused={focused ? 'true' : ''}
29
+ role="button"
30
+ tabIndex={-1}
31
+ onClick={handleClick}
32
+ padding={3}
33
+ radius={2}
34
+ selected={selected}
35
+ tone={isToday || selected ? 'primary' : 'default'}
36
+ >
37
+ <Text
38
+ muted={!selected && !isCurrentMonth}
39
+ style={{textAlign: 'center'}}
40
+ weight={isCurrentMonth ? 'medium' : 'regular'}
41
+ >
42
+ {date.getDate()}
43
+ </Text>
44
+ </Card>
45
+ </div>
46
+ )
47
+ }
@@ -0,0 +1,52 @@
1
+ import {Box, Grid, Text} from '@sanity/ui'
2
+ import {isSameDay, isSameMonth} from 'date-fns'
3
+ import React from 'react'
4
+ import {CalendarDay} from './CalendarDay'
5
+ import {WEEK_DAY_NAMES} from './constants'
6
+ import {getWeeksOfMonth} from './utils'
7
+
8
+ interface CalendarMonthProps {
9
+ date: Date
10
+ focused?: Date
11
+ selected?: Date
12
+ onSelect: (date: Date) => void
13
+ hidden?: boolean
14
+ }
15
+
16
+ export function CalendarMonth(props: CalendarMonthProps) {
17
+ return (
18
+ <Box aria-hidden={props.hidden || false} data-ui="CalendarMonth">
19
+ <Grid gap={1} style={{gridTemplateColumns: 'repeat(7, minmax(44px, 46px))'}}>
20
+ {WEEK_DAY_NAMES.map((weekday) => (
21
+ <Box key={weekday} paddingY={2}>
22
+ <Text size={1} weight="medium" style={{textAlign: 'center'}}>
23
+ {weekday}
24
+ </Text>
25
+ </Box>
26
+ ))}
27
+
28
+ {getWeeksOfMonth(props.date).map((week, weekIdx) =>
29
+ week.days.map((date, dayIdx) => {
30
+ const focused = props.focused && isSameDay(date, props.focused)
31
+ const selected = props.selected && isSameDay(date, props.selected)
32
+ const isToday = isSameDay(date, new Date())
33
+ const isCurrentMonth = props.focused && isSameMonth(date, props.focused)
34
+
35
+ return (
36
+ <CalendarDay
37
+ date={date}
38
+ focused={focused}
39
+ isCurrentMonth={isCurrentMonth}
40
+ isToday={isToday}
41
+ // eslint-disable-next-line react/no-array-index-key
42
+ key={`${weekIdx}-${dayIdx}`}
43
+ onSelect={props.onSelect}
44
+ selected={selected}
45
+ />
46
+ )
47
+ })
48
+ )}
49
+ </Grid>
50
+ </Box>
51
+ )
52
+ }
@@ -0,0 +1,22 @@
1
+ import React from 'react'
2
+ import {TextInput} from '@sanity/ui'
3
+ import {LazyTextInput} from '../LazyTextInput'
4
+
5
+ type Props = Omit<React.ComponentProps<typeof TextInput>, 'onChange' | 'value'> & {
6
+ value?: number
7
+ onChange: (year: number) => void
8
+ }
9
+
10
+ export const YearInput = ({onChange, ...props}: Props) => {
11
+ const handleChange = React.useCallback(
12
+ (event: React.FocusEvent<HTMLInputElement> | React.ChangeEvent<HTMLInputElement>) => {
13
+ const numericValue = parseInt(event.currentTarget.value, 10)
14
+ if (!isNaN(numericValue)) {
15
+ onChange(numericValue)
16
+ }
17
+ },
18
+ [onChange]
19
+ )
20
+
21
+ return <LazyTextInput {...props} onChange={handleChange} inputMode="numeric" />
22
+ }
@@ -0,0 +1,33 @@
1
+ import {range} from 'lodash'
2
+
3
+ export const MONTH_NAMES = [
4
+ 'January',
5
+ 'February',
6
+ 'March',
7
+ 'April',
8
+ 'May',
9
+ 'June',
10
+ 'July',
11
+ 'August',
12
+ 'September',
13
+ 'October',
14
+ 'November',
15
+ 'December',
16
+ ]
17
+
18
+ export const WEEK_DAY_NAMES = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
19
+
20
+ export const HOURS_24 = range(0, 24)
21
+
22
+ export const ARROW_KEYS = ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight']
23
+
24
+ export const DEFAULT_TIME_PRESETS = [
25
+ [0, 0],
26
+ [6, 0],
27
+ [12, 0],
28
+ [18, 0],
29
+ [23, 59],
30
+ ]
31
+
32
+ // all weekdays except first
33
+ export const TAIL_WEEKDAYS = [1, 2, 3, 4, 5, 6]
@@ -0,0 +1,4 @@
1
+ export const features = {
2
+ dayPresets: false,
3
+ timePresets: false,
4
+ }
@@ -0,0 +1,34 @@
1
+ import {addDays, eachWeekOfInterval, getWeek, lastDayOfMonth, startOfMonth} from 'date-fns'
2
+
3
+ import {TAIL_WEEKDAYS} from './constants'
4
+
5
+ export const getWeekStartsOfMonth = (date: Date): Date[] => {
6
+ const firstDay = startOfMonth(date)
7
+ return eachWeekOfInterval({
8
+ start: firstDay,
9
+ end: lastDayOfMonth(firstDay),
10
+ })
11
+ }
12
+
13
+ export const getWeekDaysFromWeekStarts = (weekStarts: Date[]): Date[][] => {
14
+ return weekStarts.map((weekStart) => [
15
+ weekStart,
16
+ ...TAIL_WEEKDAYS.map((d) => addDays(weekStart, d)),
17
+ ])
18
+ }
19
+
20
+ type Week = {
21
+ number: number
22
+ days: Date[]
23
+ }
24
+
25
+ export const getWeeksOfMonth = (date: Date): Week[] =>
26
+ getWeekDaysFromWeekStarts(getWeekStartsOfMonth(date)).map(
27
+ (days): Week => ({
28
+ number: getWeek(days[0]),
29
+ days,
30
+ }),
31
+ )
32
+
33
+ export const formatTime = (hours: number, minutes: number): string =>
34
+ `${`${hours}`.padStart(2, '0')}:${`${minutes}`.padStart(2, '0')}`
@@ -0,0 +1,2 @@
1
+ export {DateInput} from './DateInput'
2
+ export {DateTimeInput} from './DateTimeInput'
@@ -0,0 +1,4 @@
1
+ export type ParseResult = {isValid: boolean; date?: Date; error?: string} & (
2
+ | {isValid: true; date: Date}
3
+ | {isValid: false; error?: string}
4
+ )
@@ -0,0 +1,4 @@
1
+ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */
2
+ export function isValidDate(date: Date) {
3
+ return date instanceof Date && !isNaN(date.valueOf())
4
+ }