@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.
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
+
}
|
package/src/ui/Toolkit__Ui.res
CHANGED
|
@@ -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
|
+
}
|
package/src/vendors/Qs.res
CHANGED