@colisweb/rescript-toolkit 2.54.3 → 2.56.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@colisweb/rescript-toolkit",
3
- "version": "2.54.3",
3
+ "version": "2.56.0",
4
4
  "scripts": {
5
5
  "clean": "rescript clean",
6
6
  "build": "rescript build -with-deps",
@@ -79,7 +79,7 @@
79
79
  "rescript-react-update": "5.0.0",
80
80
  "sanitize-html": "1.27.4",
81
81
  "swr": "1.3.0",
82
- "tailwindcss": "3.1.8"
82
+ "tailwindcss": "3.2.4"
83
83
  },
84
84
  "devDependencies": {
85
85
  "@babel/core": "7.18.6",
@@ -438,3 +438,31 @@ module Coordinates = {
438
438
  longitude: gmaps.lng,
439
439
  }
440
440
  }
441
+
442
+ module DatetimeTimeSlot = {
443
+ @decco
444
+ type t = {
445
+ start: Datetime.t,
446
+ @as("end") @decco.key("end")
447
+ end_: Datetime.t,
448
+ }
449
+
450
+ let isEqual = (a: t, b: t): bool => {
451
+ a.start->Js.Date.toString === b.start->Js.Date.toString &&
452
+ a.end_->Js.Date.toString === b.end_->Js.Date.toString
453
+ }
454
+ }
455
+
456
+ module DateTimeSlot = {
457
+ @decco
458
+ type t = {
459
+ start: Date.t,
460
+ @as("end") @decco.key("end")
461
+ end_: Date.t,
462
+ }
463
+
464
+ let isEqual = (a: t, b: t): bool => {
465
+ a.start->Js.Date.toString === b.start->Js.Date.toString &&
466
+ a.end_->Js.Date.toString === b.end_->Js.Date.toString
467
+ }
468
+ }
@@ -108,7 +108,7 @@ let useDisclosure = (~defaultIsOpen=?, ()) => {
108
108
  let (isOpen, setIsOpen) = React.useState(() => defaultIsOpen->Belt.Option.getWithDefault(false))
109
109
 
110
110
  {
111
- isOpen: isOpen,
111
+ isOpen,
112
112
  show: React.useCallback(() => setIsOpen(_ => true)),
113
113
  hide: React.useCallback(() => setIsOpen(_ => false)),
114
114
  toggle: React.useCallback(() => setIsOpen(isOpen => !isOpen)),
@@ -146,7 +146,7 @@ let useClipboard = (~onCopyNotificationMessage: option<string>=?, value: string)
146
146
  : None
147
147
  , [hasCopied])
148
148
 
149
- {value: value, copy: onCopy, hasCopied: hasCopied}
149
+ {value, copy: onCopy, hasCopied}
150
150
  }
151
151
 
152
152
  // ----------------------
@@ -211,7 +211,7 @@ let useMediaQuery = () => {
211
211
  let isXs = React.useMemo0(() => matchMedia("(max-width: 639px)").matches)
212
212
  let isSm = React.useMemo0(() => matchMedia("(min-width: 640px)").matches)
213
213
  let isLg = React.useMemo0(() => matchMedia("(min-width: 1024px)").matches)
214
- {isXs: isXs, isSm: isSm, isLg: isLg}
214
+ {isXs, isSm, isLg}
215
215
  }
216
216
 
217
217
  let useOnClickOutside = (ref: React.ref<Js.Nullable.t<Dom.element>>, handler) => {
@@ -268,3 +268,21 @@ let useIsVisibleOnViewport = (~options: option<ReactUse.intersectionOptions<'a>>
268
268
 
269
269
  isVisible
270
270
  }
271
+
272
+ let useQueryParams = (~decoder, ~defaultParams) => {
273
+ let queryString = {
274
+ open Browser.Location
275
+ location->search
276
+ }
277
+
278
+ React.useMemo1(
279
+ () =>
280
+ queryString
281
+ ->Js.String2.sliceToEnd(~from=1)
282
+ ->Qs.parse
283
+ ->Obj.magic
284
+ ->decoder
285
+ ->Result.getWithDefault(defaultParams),
286
+ [queryString],
287
+ )
288
+ }
@@ -33,3 +33,4 @@ module MultiSelect = Toolkit__Ui_MultiSelect
33
33
  module Notice = Toolkit__Ui_Notice
34
34
  module NativeDatePicker = Toolkit__Ui_NativeDatePicker
35
35
  module ErrorBoundary = Toolkit__Ui_ErrorBoundary
36
+ module WeekDateFilter = Toolkit__Ui_WeekDateFilter
@@ -0,0 +1,215 @@
1
+ open ReactIntl
2
+
3
+ type state = {
4
+ timeslot: Toolkit__Decoders.DatetimeTimeSlot.t,
5
+ selectedDay: option<Js.Date.t>,
6
+ }
7
+
8
+ type action =
9
+ | PreviousTimeSlot
10
+ | NextTimeSlot
11
+ | UpdateSelectedDay(option<Js.Date.t>)
12
+
13
+ @decco
14
+ type queryParams = {
15
+ start: Toolkit__Decoders.Option.t<Toolkit__Decoders.Date.t>,
16
+ selectedDay: Toolkit__Decoders.Option.t<Toolkit__Decoders.Date.t>,
17
+ }
18
+
19
+ @react.component
20
+ let make = (~initialStart=?, ~selectedDay=?, ~children: state => React.element) => {
21
+ let {isSm} = Toolkit__Hooks.useMediaQuery()
22
+ let intl = useIntl()
23
+ let timeSlotLength = isSm ? 7 : 3
24
+
25
+ let queryParams = Toolkit__Hooks.useQueryParams(
26
+ ~decoder=queryParams_decode,
27
+ ~defaultParams={
28
+ start: None,
29
+ selectedDay: None,
30
+ },
31
+ )
32
+
33
+ // Note: not sure about priority there...
34
+ let initialStart = switch (initialStart, queryParams.start) {
35
+ | (Some(_) as date, _) => date
36
+ | (_, Some(_) as date) => date
37
+ | _ => None
38
+ }
39
+ let selectedDay = switch (selectedDay, queryParams.selectedDay) {
40
+ | (Some(_) as date, _) => date
41
+ | (_, Some(_) as date) => date
42
+ | _ => None
43
+ }
44
+
45
+ let ({timeslot, selectedDay}, dispatch) = ReactUpdate.useReducerWithMapState(
46
+ (state, action) =>
47
+ switch action {
48
+ | PreviousTimeSlot =>
49
+ UpdateWithSideEffects(
50
+ {
51
+ timeslot: {
52
+ start: state.timeslot.start->BsDateFns.subDays(timeSlotLength)->BsDateFns.startOfDay,
53
+ end_: state.timeslot.start->BsDateFns.subDays(1),
54
+ },
55
+ selectedDay: state.selectedDay->Option.map(_ =>
56
+ state.timeslot.start->BsDateFns.subDays(timeSlotLength)->BsDateFns.startOfDay
57
+ ),
58
+ },
59
+ ({state}) => {
60
+ RescriptReactRouter.replace(
61
+ Qs.stringifyWithParams(
62
+ {
63
+ start: Some(state.timeslot.start),
64
+ selectedDay: state.selectedDay,
65
+ }->queryParams_encode,
66
+ {
67
+ addQueryPrefix: true,
68
+ serializeDate: d => d->BsDateFns.formatWithPattern("yyyy-MM-dd"),
69
+ },
70
+ ),
71
+ )
72
+ None
73
+ },
74
+ )
75
+ | NextTimeSlot =>
76
+ UpdateWithSideEffects(
77
+ {
78
+ timeslot: {
79
+ start: state.timeslot.end_->BsDateFns.addDays(1),
80
+ end_: state.timeslot.end_->BsDateFns.addDays(timeSlotLength),
81
+ },
82
+ selectedDay: state.selectedDay->Option.map(_ =>
83
+ state.timeslot.end_->BsDateFns.addDays(1)
84
+ ),
85
+ },
86
+ ({state}) => {
87
+ RescriptReactRouter.replace(
88
+ Qs.stringifyWithParams(
89
+ {
90
+ start: Some(state.timeslot.start),
91
+ selectedDay: state.selectedDay,
92
+ }->queryParams_encode,
93
+ {
94
+ addQueryPrefix: true,
95
+ serializeDate: d => d->BsDateFns.formatWithPattern("yyyy-MM-dd"),
96
+ },
97
+ ),
98
+ )
99
+ None
100
+ },
101
+ )
102
+ | UpdateSelectedDay(date) =>
103
+ UpdateWithSideEffects(
104
+ {...state, selectedDay: date},
105
+ ({state}) => {
106
+ RescriptReactRouter.replace(
107
+ Qs.stringifyWithParams(
108
+ {
109
+ start: Some(state.timeslot.start),
110
+ selectedDay: state.selectedDay,
111
+ }->queryParams_encode,
112
+ {
113
+ addQueryPrefix: true,
114
+ serializeDate: d => d->BsDateFns.formatWithPattern("yyyy-MM-dd"),
115
+ },
116
+ ),
117
+ )
118
+ None
119
+ },
120
+ )
121
+ },
122
+ () => {
123
+ let defaultTimeslot: Toolkit__Decoders.DatetimeTimeSlot.t = {
124
+ let today = Js.Date.make()
125
+ let timeSlotStart = isSm ? BsDateFns.startOfWeek(today, {weekStartsOn: 1}) : today
126
+
127
+ let timeSlotEnd = isSm
128
+ ? BsDateFns.endOfWeek(today, {weekStartsOn: 1})
129
+ : today->BsDateFns.addDays(2)
130
+ {
131
+ start: timeSlotStart,
132
+ end_: timeSlotEnd,
133
+ }
134
+ }
135
+
136
+ {
137
+ timeslot: initialStart->Option.mapWithDefault(defaultTimeslot, start => {
138
+ start,
139
+ end_: start->BsDateFns.addDays(timeSlotLength - 1),
140
+ }),
141
+ selectedDay: selectedDay->Option.isSome
142
+ ? selectedDay
143
+ : isSm
144
+ ? None
145
+ : Some(initialStart->Option.getWithDefault(defaultTimeslot.start)),
146
+ }
147
+ },
148
+ )
149
+
150
+ <React.Fragment>
151
+ <div className="bg-white rounded flex justify-center items-center p-6">
152
+ <Toolkit__Ui_IconButton
153
+ size=#xs
154
+ variant=#outline
155
+ color=#neutral
156
+ onClick={_ => dispatch(PreviousTimeSlot)}
157
+ ariaLabel="previous week"
158
+ icon={<BsReactIcons.MdKeyboardArrowLeft size=30 />}
159
+ />
160
+ <div className="flex items-center mx-2">
161
+ {BsDateFns.eachDayOfInterval({
162
+ start: timeslot.start,
163
+ end_: timeslot.end_,
164
+ })
165
+ ->Array.mapWithIndex((i, day) => {
166
+ let isSelected =
167
+ selectedDay->Option.mapWithDefault(false, sDay => BsDateFns.isSameDay(sDay, day))
168
+ <React.Fragment key={`${i->Int.toString}-day`}>
169
+ {i == 0 || day->BsDateFns.isFirstDayOfMonth
170
+ ? <p className="text-xs text-neutral-600 uppercase">
171
+ <FormattedDate value=day month=#short />
172
+ </p>
173
+ : React.null}
174
+ <div
175
+ onClick={_ => dispatch(UpdateSelectedDay(isSm && isSelected ? None : Some(day)))}
176
+ className="flex flex-col items-stretch w-16 mx-1 font-display cursor-pointer">
177
+ <p
178
+ className={cx([
179
+ "flex flex-col items-center justify-center uppercase text-xs w-full rounded-sm leading-tight py-1",
180
+ isSelected ? "text-white bg-primary-700" : "text-primary-700 bg-primary-50",
181
+ ])}>
182
+ <span>
183
+ {
184
+ let day =
185
+ intl->Intl.formatDateWithOptions(
186
+ day,
187
+ dateTimeFormatOptions(~weekday=#long, ()),
188
+ )
189
+ day
190
+ ->Js.String2.slice(~from=0, ~to_=4)
191
+ ->Js.String2.concat(day->Js.String2.length > 4 ? "." : "")
192
+ ->React.string
193
+ }
194
+ </span>
195
+ <span className="text-sm font-semibold">
196
+ <FormattedDate value=day day=#"2-digit" />
197
+ </span>
198
+ </p>
199
+ </div>
200
+ </React.Fragment>
201
+ })
202
+ ->React.array}
203
+ </div>
204
+ <Toolkit__Ui_IconButton
205
+ size=#xs
206
+ variant=#outline
207
+ color=#neutral
208
+ onClick={_ => dispatch(NextTimeSlot)}
209
+ ariaLabel="next week"
210
+ icon={<BsReactIcons.MdKeyboardArrowRight size=30 />}
211
+ />
212
+ </div>
213
+ {children({timeslot, selectedDay})}
214
+ </React.Fragment>
215
+ }
@@ -1,8 +1,8 @@
1
1
  @module("qs") external stringify: Js.t<'a> => string = "stringify"
2
2
 
3
3
  type params = {
4
- addQueryPrefix: bool,
5
- serializeDate: Js.Date.t => string,
4
+ addQueryPrefix?: bool,
5
+ serializeDate?: Js.Date.t => string,
6
6
  }
7
7
  @obj
8
8
  external makeParams: (