dce-reactkit 3.0.0-beta.9 → 3.0.2
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/README.md +1 -546
- package/dist/cjs/index.js +945 -83
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/types/components/AppWrapper.d.ts +25 -0
- package/dist/cjs/types/components/ButtonInputGroup.d.ts +12 -0
- package/dist/cjs/types/components/CheckboxButton.d.ts +20 -0
- package/dist/cjs/types/components/RadioButton.d.ts +20 -0
- package/dist/cjs/types/components/SimpleDateChooser.d.ts +22 -0
- package/dist/cjs/types/components/TabBox.d.ts +1 -0
- package/dist/cjs/types/helpers/genRouteHandler.d.ts +32 -0
- package/dist/cjs/types/helpers/getOrdinal.d.ts +8 -0
- package/dist/cjs/types/helpers/getTimeInfoInET.d.ts +17 -0
- package/dist/cjs/types/helpers/handleError.d.ts +18 -0
- package/dist/cjs/types/helpers/handleSuccess.d.ts +8 -0
- package/dist/cjs/types/helpers/visitServerEndpoint.d.ts +23 -0
- package/dist/cjs/types/index.d.ts +13 -1
- package/dist/cjs/types/server/initServer.d.ts +21 -0
- package/dist/cjs/types/types/ParamType.d.ts +17 -0
- package/dist/cjs/types/types/ReactKitErrorCode.d.ts +3 -1
- package/dist/esm/index.js +934 -84
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/types/components/AppWrapper.d.ts +25 -0
- package/dist/esm/types/components/ButtonInputGroup.d.ts +12 -0
- package/dist/esm/types/components/CheckboxButton.d.ts +20 -0
- package/dist/esm/types/components/RadioButton.d.ts +20 -0
- package/dist/esm/types/components/SimpleDateChooser.d.ts +22 -0
- package/dist/esm/types/components/TabBox.d.ts +1 -0
- package/dist/esm/types/helpers/genRouteHandler.d.ts +32 -0
- package/dist/esm/types/helpers/getOrdinal.d.ts +8 -0
- package/dist/esm/types/helpers/getTimeInfoInET.d.ts +17 -0
- package/dist/esm/types/helpers/handleError.d.ts +18 -0
- package/dist/esm/types/helpers/handleSuccess.d.ts +8 -0
- package/dist/esm/types/helpers/visitServerEndpoint.d.ts +23 -0
- package/dist/esm/types/index.d.ts +13 -1
- package/dist/esm/types/server/initServer.d.ts +21 -0
- package/dist/esm/types/types/ParamType.d.ts +17 -0
- package/dist/esm/types/types/ReactKitErrorCode.d.ts +3 -1
- package/dist/index.d.ts +231 -10
- package/package.json +3 -1
- package/rollup.config.js +0 -1
- package/src/components/AppWrapper.tsx +73 -11
- package/src/components/ButtonInputGroup.tsx +89 -0
- package/src/components/CheckboxButton.tsx +100 -0
- package/src/components/ErrorBox.tsx +4 -4
- package/src/components/LoadingSpinner.tsx +3 -3
- package/src/components/Modal.tsx +184 -23
- package/src/components/RadioButton.tsx +102 -0
- package/src/components/SimpleDateChooser.tsx +222 -0
- package/src/components/TabBox.tsx +65 -58
- package/src/helpers/genRouteHandler.ts +329 -0
- package/src/helpers/getOrdinal.tsx +13 -0
- package/src/helpers/getTimeInfoInET.tsx +56 -0
- package/src/helpers/handleError.ts +65 -0
- package/src/helpers/handleSuccess.ts +18 -0
- package/src/helpers/visitServerEndpoint.tsx +110 -0
- package/src/index.ts +27 -0
- package/src/server/initServer.ts +53 -0
- package/src/types/ParamType.ts +18 -0
- package/src/types/ReactKitErrorCode.tsx +3 -1
|
@@ -63,9 +63,9 @@ const ErrorBox: React.FC<Props> = (props) => {
|
|
|
63
63
|
<span
|
|
64
64
|
style={{
|
|
65
65
|
backgroundColor: 'white',
|
|
66
|
-
borderRadius: '
|
|
67
|
-
paddingLeft: '
|
|
68
|
-
paddingRight: '
|
|
66
|
+
borderRadius: '0.3rem',
|
|
67
|
+
paddingLeft: '0.2rem',
|
|
68
|
+
paddingRight: '0.2rem',
|
|
69
69
|
color: '#DC4150',
|
|
70
70
|
fontVariant: 'small-caps',
|
|
71
71
|
fontSize: '80%',
|
|
@@ -85,7 +85,7 @@ const ErrorBox: React.FC<Props> = (props) => {
|
|
|
85
85
|
<div
|
|
86
86
|
className="alert alert-danger text-center"
|
|
87
87
|
style={{
|
|
88
|
-
maxWidth: '
|
|
88
|
+
maxWidth: '40rem',
|
|
89
89
|
margin: 'auto',
|
|
90
90
|
}}
|
|
91
91
|
>
|
|
@@ -20,10 +20,10 @@ const style = `
|
|
|
20
20
|
.LoadingSpinner-blip-2,
|
|
21
21
|
.LoadingSpinner-blip-3,
|
|
22
22
|
.LoadingSpinner-blip-4 {
|
|
23
|
-
font-size:
|
|
23
|
+
font-size: 1.8rem;
|
|
24
24
|
opacity: 0.6;
|
|
25
|
-
margin-top:
|
|
26
|
-
margin-bottom:
|
|
25
|
+
margin-top: 1rem;
|
|
26
|
+
margin-bottom: 1rem;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
/* First Blip */
|
package/src/components/Modal.tsx
CHANGED
|
@@ -15,21 +15,12 @@ import ModalButtonType from '../types/ModalButtonType';
|
|
|
15
15
|
import ModalSize from '../types/ModalSize';
|
|
16
16
|
import ModalType from '../types/ModalType';
|
|
17
17
|
|
|
18
|
-
/*------------------------------------------------------------------------*/
|
|
19
|
-
/* Style */
|
|
20
|
-
/*------------------------------------------------------------------------*/
|
|
21
|
-
|
|
22
|
-
const style = `
|
|
23
|
-
`;
|
|
24
|
-
|
|
25
18
|
/*------------------------------------------------------------------------*/
|
|
26
19
|
/* Constants */
|
|
27
20
|
/*------------------------------------------------------------------------*/
|
|
28
21
|
|
|
29
22
|
// Constants
|
|
30
|
-
const MS_TO_ANIMATE =
|
|
31
|
-
const MS_ANIMATE_IN_DELAY = 10;
|
|
32
|
-
// Time to wait before animating in (must be >0 or animation won't trigger)
|
|
23
|
+
const MS_TO_ANIMATE = 200; // Animation duration
|
|
33
24
|
|
|
34
25
|
// Modal type to list of buttons
|
|
35
26
|
const modalTypeToModalButtonTypes: {
|
|
@@ -113,6 +104,116 @@ const ModalButtonTypeToLabelAndVariant = {
|
|
|
113
104
|
},
|
|
114
105
|
};
|
|
115
106
|
|
|
107
|
+
/*------------------------------------------------------------------------*/
|
|
108
|
+
/* Style */
|
|
109
|
+
/*------------------------------------------------------------------------*/
|
|
110
|
+
|
|
111
|
+
const style = `
|
|
112
|
+
.Modal-backdrop {
|
|
113
|
+
position: fixed;
|
|
114
|
+
top: 0;
|
|
115
|
+
left: 0;
|
|
116
|
+
width: 100vw;
|
|
117
|
+
height: 100vw;
|
|
118
|
+
background-color: rgba(0, 0, 0, 0.7);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.Modal-fading-in {
|
|
122
|
+
animation-name: Modal-fading-in;
|
|
123
|
+
animation-duration: ${Math.floor(MS_TO_ANIMATE * 2)}ms;
|
|
124
|
+
animation-iteration-count: 1;
|
|
125
|
+
animation-fill-mode: both;
|
|
126
|
+
animation-timing-function: ease-out;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
@keyframes Modal-fading-in {
|
|
130
|
+
0% {
|
|
131
|
+
opacity: 0;
|
|
132
|
+
}
|
|
133
|
+
100% {
|
|
134
|
+
opacity: 1;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.Modal-fading-out {
|
|
139
|
+
animation-name: Modal-fading-out;
|
|
140
|
+
animation-duration: ${MS_TO_ANIMATE}ms;
|
|
141
|
+
animation-iteration-count: 1;
|
|
142
|
+
animation-fill-mode: both;
|
|
143
|
+
animation-timing-function: ease-in;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
@keyframes Modal-fading-out {
|
|
147
|
+
0% {
|
|
148
|
+
opacity: 1;
|
|
149
|
+
}
|
|
150
|
+
100% {
|
|
151
|
+
opacity: 0;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
.Modal-animating-in {
|
|
156
|
+
animation-name: Modal-animating-in;
|
|
157
|
+
animation-duration: ${MS_TO_ANIMATE}ms;
|
|
158
|
+
animation-iteration-count: 1;
|
|
159
|
+
animation-fill-mode: both;
|
|
160
|
+
animation-timing-function: ease-out;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
@keyframes Modal-animating-in {
|
|
164
|
+
0% {
|
|
165
|
+
transform: scale(1.05) translate(0, -1.5rem);
|
|
166
|
+
opacity: 0;
|
|
167
|
+
}
|
|
168
|
+
100% {
|
|
169
|
+
transform: scale(1) translate(0, 0);
|
|
170
|
+
opacity: 1;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
.Modal-animating-pop {
|
|
175
|
+
animation-name: Modal-animating-pop;
|
|
176
|
+
animation-duration: ${MS_TO_ANIMATE}ms;
|
|
177
|
+
animation-iteration-count: 1;
|
|
178
|
+
animation-fill-mode: both;
|
|
179
|
+
animation-timing-function: ease-in-out;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
@keyframes Modal-animating-pop {
|
|
183
|
+
0% {
|
|
184
|
+
transform: scale(1);
|
|
185
|
+
opacity: 1;
|
|
186
|
+
}
|
|
187
|
+
50% {
|
|
188
|
+
transform: scale(1.05);
|
|
189
|
+
opacity: 0.9;
|
|
190
|
+
}
|
|
191
|
+
100% {
|
|
192
|
+
transform: scale(1);
|
|
193
|
+
opacity: 1;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
.Modal-animating-out {
|
|
198
|
+
animation-name: Modal-animating-out;
|
|
199
|
+
animation-duration: ${MS_TO_ANIMATE}ms;
|
|
200
|
+
animation-iteration-count: 1;
|
|
201
|
+
animation-fill-mode: both;
|
|
202
|
+
animation-timing-function: ease-in;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
@keyframes Modal-animating-out {
|
|
206
|
+
0% {
|
|
207
|
+
transform: scale(1) translate(0, 0);
|
|
208
|
+
opacity: 1;
|
|
209
|
+
}
|
|
210
|
+
100% {
|
|
211
|
+
transform: scale(1.05) translate(0, -1.5rem);
|
|
212
|
+
opacity: 0;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
`;
|
|
216
|
+
|
|
116
217
|
/*------------------------------------------------------------------------*/
|
|
117
218
|
/* Types */
|
|
118
219
|
/*------------------------------------------------------------------------*/
|
|
@@ -197,11 +298,17 @@ const Modal: React.FC<Props> = (props) => {
|
|
|
197
298
|
|
|
198
299
|
/* -------------- State ------------- */
|
|
199
300
|
|
|
301
|
+
// If true, the modal is completely gone
|
|
302
|
+
// (not just invisible, also removed from the dom)
|
|
303
|
+
const [gone, setGone] = useState(false);
|
|
304
|
+
|
|
200
305
|
// If true, the modal is shown
|
|
201
306
|
const [visible, setVisible] = useState(false);
|
|
202
307
|
|
|
203
|
-
// True if
|
|
204
|
-
const [animatingIn, setAnimatingIn] = useState(
|
|
308
|
+
// True if animation is in use
|
|
309
|
+
const [animatingIn, setAnimatingIn] = useState(true);
|
|
310
|
+
const [animatingPop, setAnimatingPop] = useState(false);
|
|
311
|
+
const [animatingOut, setAnimatingOut] = useState(false);
|
|
205
312
|
|
|
206
313
|
/*------------------------------------------------------------------------*/
|
|
207
314
|
/* Lifecycle Functions */
|
|
@@ -214,13 +321,10 @@ const Modal: React.FC<Props> = (props) => {
|
|
|
214
321
|
useEffect(
|
|
215
322
|
() => {
|
|
216
323
|
(async () => {
|
|
217
|
-
// Start the component animating in
|
|
218
|
-
await waitMs(MS_ANIMATE_IN_DELAY);
|
|
219
|
-
setAnimatingIn(true);
|
|
220
|
-
|
|
221
324
|
// Wait and then set visible to true
|
|
222
325
|
await waitMs(MS_TO_ANIMATE);
|
|
223
326
|
setVisible(true);
|
|
327
|
+
setAnimatingIn(false);
|
|
224
328
|
})();
|
|
225
329
|
},
|
|
226
330
|
[],
|
|
@@ -254,16 +358,23 @@ const Modal: React.FC<Props> = (props) => {
|
|
|
254
358
|
|
|
255
359
|
// Update the state
|
|
256
360
|
setVisible(false);
|
|
361
|
+
setAnimatingOut(true);
|
|
257
362
|
|
|
258
363
|
// Call the handler after the modal has animated out
|
|
259
364
|
await waitMs(MS_TO_ANIMATE);
|
|
260
365
|
onClose(ModalButtonType);
|
|
366
|
+
setGone(true);
|
|
261
367
|
};
|
|
262
368
|
|
|
263
369
|
/*------------------------------------------------------------------------*/
|
|
264
370
|
/* Render */
|
|
265
371
|
/*------------------------------------------------------------------------*/
|
|
266
372
|
|
|
373
|
+
// Return nothing if gone
|
|
374
|
+
if (gone) {
|
|
375
|
+
return null;
|
|
376
|
+
}
|
|
377
|
+
|
|
267
378
|
/*----------------------------------------*/
|
|
268
379
|
/* Footer */
|
|
269
380
|
/*----------------------------------------*/
|
|
@@ -295,8 +406,9 @@ const Modal: React.FC<Props> = (props) => {
|
|
|
295
406
|
// Create the button
|
|
296
407
|
return (
|
|
297
408
|
<button
|
|
409
|
+
key={ModalButtonType}
|
|
298
410
|
type="button"
|
|
299
|
-
className={`Modal-${ModalButtonType}-button btn btn-${variant} ${last ? '' : '
|
|
411
|
+
className={`Modal-${ModalButtonType}-button btn btn-${variant} ${last ? '' : 'me-1'}`}
|
|
300
412
|
onClick={() => {
|
|
301
413
|
handleClose(ModalButtonType);
|
|
302
414
|
}}
|
|
@@ -317,24 +429,73 @@ const Modal: React.FC<Props> = (props) => {
|
|
|
317
429
|
: undefined
|
|
318
430
|
);
|
|
319
431
|
|
|
432
|
+
// Choose an animation
|
|
433
|
+
let animationClass = '';
|
|
434
|
+
let backdropAnimationClass = '';
|
|
435
|
+
if (animatingIn) {
|
|
436
|
+
animationClass = 'Modal-animating-in';
|
|
437
|
+
backdropAnimationClass = 'Modal-fading-in';
|
|
438
|
+
} else if (animatingOut) {
|
|
439
|
+
animationClass = 'Modal-animating-out';
|
|
440
|
+
backdropAnimationClass = 'Modal-fading-out';
|
|
441
|
+
} else if (animatingPop) {
|
|
442
|
+
animationClass = 'Modal-animating-pop';
|
|
443
|
+
}
|
|
444
|
+
|
|
320
445
|
// Render the modal
|
|
321
446
|
return (
|
|
322
447
|
<div
|
|
323
|
-
className={`modal
|
|
448
|
+
className={`modal show modal-dialog-scrollable modal-dialog-centered modal-${size}`}
|
|
324
449
|
tabIndex={-1}
|
|
325
450
|
style={{
|
|
326
451
|
zIndex: (
|
|
327
452
|
onTopOfOtherModals
|
|
328
|
-
?
|
|
453
|
+
? 5000000001
|
|
329
454
|
: 5000000000
|
|
330
455
|
),
|
|
331
456
|
display: 'block',
|
|
457
|
+
margin: 'auto',
|
|
458
|
+
left: 0,
|
|
459
|
+
right: 0,
|
|
332
460
|
}}
|
|
333
461
|
>
|
|
334
|
-
<
|
|
462
|
+
<style>{style}</style>
|
|
463
|
+
<div
|
|
464
|
+
className={`Modal-backdrop ${backdropAnimationClass}`}
|
|
465
|
+
style={{
|
|
466
|
+
zIndex: 5000000003,
|
|
467
|
+
}}
|
|
468
|
+
onClick={async () => {
|
|
469
|
+
// Skip if exit via backdrop not allowed
|
|
470
|
+
if (dontAllowBackdropExit || !onClose) {
|
|
471
|
+
// Show pop animation
|
|
472
|
+
if (!animatingPop) {
|
|
473
|
+
setAnimatingPop(true);
|
|
474
|
+
|
|
475
|
+
// Wait then stop pop animation
|
|
476
|
+
await waitMs(MS_TO_ANIMATE);
|
|
477
|
+
setAnimatingPop(false);
|
|
478
|
+
}
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
// Handle close
|
|
482
|
+
handleClose(ModalButtonType.Cancel);
|
|
483
|
+
}}
|
|
484
|
+
/>
|
|
485
|
+
<div
|
|
486
|
+
className={`modal-dialog ${animationClass}`}
|
|
487
|
+
style={{
|
|
488
|
+
zIndex: 5000000002,
|
|
489
|
+
}}
|
|
490
|
+
>
|
|
335
491
|
<div className="modal-content">
|
|
336
492
|
<div className="modal-header">
|
|
337
|
-
<h5
|
|
493
|
+
<h5
|
|
494
|
+
className="modal-title"
|
|
495
|
+
style={{
|
|
496
|
+
fontWeight: 'bold',
|
|
497
|
+
}}
|
|
498
|
+
>
|
|
338
499
|
{title}
|
|
339
500
|
</h5>
|
|
340
501
|
|
|
@@ -342,9 +503,9 @@ const Modal: React.FC<Props> = (props) => {
|
|
|
342
503
|
<button
|
|
343
504
|
type="button"
|
|
344
505
|
className="btn-close"
|
|
345
|
-
data-bs-dismiss="modal"
|
|
346
506
|
aria-label="Close"
|
|
347
507
|
onClick={() => {
|
|
508
|
+
// Handle close
|
|
348
509
|
handleClose(ModalButtonType.Cancel);
|
|
349
510
|
}}
|
|
350
511
|
/>
|
|
@@ -356,7 +517,7 @@ const Modal: React.FC<Props> = (props) => {
|
|
|
356
517
|
</div>
|
|
357
518
|
)}
|
|
358
519
|
{footer && (
|
|
359
|
-
<div className="modal-footer">
|
|
520
|
+
<div className="modal-footer pt-1 pb-1">
|
|
360
521
|
{footer}
|
|
361
522
|
</div>
|
|
362
523
|
)}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A radio selection button
|
|
3
|
+
* @author Gabe Abrams
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Import React
|
|
7
|
+
import React from 'react';
|
|
8
|
+
|
|
9
|
+
// Import FontAwesome Icons
|
|
10
|
+
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|
11
|
+
import { faDotCircle } from '@fortawesome/free-solid-svg-icons';
|
|
12
|
+
import { faCircle } from '@fortawesome/free-regular-svg-icons';
|
|
13
|
+
|
|
14
|
+
// Import shared types
|
|
15
|
+
import Variant from '../types/Variant';
|
|
16
|
+
|
|
17
|
+
/*------------------------------------------------------------------------*/
|
|
18
|
+
/* Types */
|
|
19
|
+
/*------------------------------------------------------------------------*/
|
|
20
|
+
|
|
21
|
+
type Props = {
|
|
22
|
+
// Text of the button
|
|
23
|
+
text: string,
|
|
24
|
+
// Handler for when this item is selected (not called if already selected)
|
|
25
|
+
onSelected: () => void,
|
|
26
|
+
// Aria label
|
|
27
|
+
ariaLabel: string,
|
|
28
|
+
// Button title
|
|
29
|
+
title?: string,
|
|
30
|
+
// True if selected
|
|
31
|
+
selected?: boolean,
|
|
32
|
+
// The id of the button
|
|
33
|
+
id?: string,
|
|
34
|
+
// If true, no margin on right
|
|
35
|
+
noMarginOnRight?: boolean,
|
|
36
|
+
// Variant for when radio is selected
|
|
37
|
+
selectedVariant?: Variant,
|
|
38
|
+
// Variant for when radio is not selected
|
|
39
|
+
unselectedVariant?: Variant,
|
|
40
|
+
// True if using a small button
|
|
41
|
+
small?: boolean,
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/*------------------------------------------------------------------------*/
|
|
45
|
+
/* Component */
|
|
46
|
+
/*------------------------------------------------------------------------*/
|
|
47
|
+
|
|
48
|
+
const RadioButton: React.FC<Props> = (props) => {
|
|
49
|
+
/*------------------------------------------------------------------------*/
|
|
50
|
+
/* Setup */
|
|
51
|
+
/*------------------------------------------------------------------------*/
|
|
52
|
+
|
|
53
|
+
/* -------------- Props ------------- */
|
|
54
|
+
|
|
55
|
+
const {
|
|
56
|
+
text,
|
|
57
|
+
onSelected,
|
|
58
|
+
ariaLabel,
|
|
59
|
+
title,
|
|
60
|
+
selected,
|
|
61
|
+
id,
|
|
62
|
+
noMarginOnRight,
|
|
63
|
+
selectedVariant = Variant.Secondary,
|
|
64
|
+
unselectedVariant = Variant.Light,
|
|
65
|
+
small,
|
|
66
|
+
} = props;
|
|
67
|
+
|
|
68
|
+
/*------------------------------------------------------------------------*/
|
|
69
|
+
/* Render */
|
|
70
|
+
/*------------------------------------------------------------------------*/
|
|
71
|
+
|
|
72
|
+
/*----------------------------------------*/
|
|
73
|
+
/* Main UI */
|
|
74
|
+
/*----------------------------------------*/
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<button
|
|
78
|
+
type="button"
|
|
79
|
+
id={id}
|
|
80
|
+
title={title}
|
|
81
|
+
className={`btn btn-${selected ? selectedVariant : unselectedVariant}${selected ? ' selected' : ''}${small ? ' btn-sm' : ''} m-0${noMarginOnRight ? '' : ' me-1'}`}
|
|
82
|
+
aria-label={`${ariaLabel}${selected ? ': currently selected' : ''}`}
|
|
83
|
+
onClick={() => {
|
|
84
|
+
if (!selected) {
|
|
85
|
+
onSelected();
|
|
86
|
+
}
|
|
87
|
+
}}
|
|
88
|
+
>
|
|
89
|
+
<FontAwesomeIcon
|
|
90
|
+
icon={selected ? faDotCircle : faCircle}
|
|
91
|
+
className="me-1"
|
|
92
|
+
/>
|
|
93
|
+
{text}
|
|
94
|
+
</button>
|
|
95
|
+
);
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/*------------------------------------------------------------------------*/
|
|
99
|
+
/* Wrap up */
|
|
100
|
+
/*------------------------------------------------------------------------*/
|
|
101
|
+
|
|
102
|
+
export default RadioButton;
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A very simple, lightweight date chooser
|
|
3
|
+
* @author Gabe Abrams
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Import React
|
|
7
|
+
import React from 'react';
|
|
8
|
+
|
|
9
|
+
// Import helpers
|
|
10
|
+
import getOrdinal from '../helpers/getOrdinal';
|
|
11
|
+
import getTimeInfoInET from '../helpers/getTimeInfoInET';
|
|
12
|
+
|
|
13
|
+
/*------------------------------------------------------------------------*/
|
|
14
|
+
/* Types */
|
|
15
|
+
/*------------------------------------------------------------------------*/
|
|
16
|
+
|
|
17
|
+
type Props = {
|
|
18
|
+
// Aria label
|
|
19
|
+
ariaLabel: string,
|
|
20
|
+
// Name of the chooser (machine-readable, hyphenated)
|
|
21
|
+
name: string,
|
|
22
|
+
// Currently selected month
|
|
23
|
+
month: number,
|
|
24
|
+
// Currently selected day
|
|
25
|
+
day: number,
|
|
26
|
+
// Currently selected year
|
|
27
|
+
year: number,
|
|
28
|
+
/**
|
|
29
|
+
* Handler for when date changes
|
|
30
|
+
* @param month new 1-indexed month number
|
|
31
|
+
* @param day new 1-indexed day number
|
|
32
|
+
* @param year new full year number
|
|
33
|
+
*/
|
|
34
|
+
onChange: (month: number, day: number, year: number) => void,
|
|
35
|
+
// Number of months in the future to allow the user to schedule into
|
|
36
|
+
// (max is 12)
|
|
37
|
+
numMonthsToShow?: number,
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/*------------------------------------------------------------------------*/
|
|
41
|
+
/* Constants */
|
|
42
|
+
/*------------------------------------------------------------------------*/
|
|
43
|
+
|
|
44
|
+
// Constants
|
|
45
|
+
const MONTH_NAMES = [
|
|
46
|
+
'January',
|
|
47
|
+
'February',
|
|
48
|
+
'March',
|
|
49
|
+
'April',
|
|
50
|
+
'May',
|
|
51
|
+
'June',
|
|
52
|
+
'July',
|
|
53
|
+
'August',
|
|
54
|
+
'September',
|
|
55
|
+
'October',
|
|
56
|
+
'November',
|
|
57
|
+
'December',
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
/*------------------------------------------------------------------------*/
|
|
61
|
+
/* Component */
|
|
62
|
+
/*------------------------------------------------------------------------*/
|
|
63
|
+
|
|
64
|
+
const SimpleDateChooser: React.FC<Props> = (props) => {
|
|
65
|
+
/*------------------------------------------------------------------------*/
|
|
66
|
+
/* Setup */
|
|
67
|
+
/*------------------------------------------------------------------------*/
|
|
68
|
+
|
|
69
|
+
/* -------------- Props ------------- */
|
|
70
|
+
|
|
71
|
+
const {
|
|
72
|
+
ariaLabel,
|
|
73
|
+
name,
|
|
74
|
+
month,
|
|
75
|
+
day,
|
|
76
|
+
year,
|
|
77
|
+
onChange,
|
|
78
|
+
} = props;
|
|
79
|
+
const numMonthsToShow = Math.min(props.numMonthsToShow ?? 6, 12);
|
|
80
|
+
|
|
81
|
+
/*------------------------------------------------------------------------*/
|
|
82
|
+
/* Render */
|
|
83
|
+
/*------------------------------------------------------------------------*/
|
|
84
|
+
|
|
85
|
+
/*----------------------------------------*/
|
|
86
|
+
/* Main UI */
|
|
87
|
+
/*----------------------------------------*/
|
|
88
|
+
|
|
89
|
+
// Determine the set of choices allowed
|
|
90
|
+
const today = getTimeInfoInET();
|
|
91
|
+
const choices: {
|
|
92
|
+
choiceName: string,
|
|
93
|
+
month: number,
|
|
94
|
+
days: number[],
|
|
95
|
+
year: number,
|
|
96
|
+
}[] = [];
|
|
97
|
+
for (let i = 0; i < numMonthsToShow; i++) {
|
|
98
|
+
// Get month and year info
|
|
99
|
+
const unmoddedMonth = (today.month + i);
|
|
100
|
+
const month = (
|
|
101
|
+
unmoddedMonth > 12
|
|
102
|
+
? unmoddedMonth - 12
|
|
103
|
+
: unmoddedMonth
|
|
104
|
+
);
|
|
105
|
+
const monthName = MONTH_NAMES[month - 1];
|
|
106
|
+
const year = (
|
|
107
|
+
unmoddedMonth > 12
|
|
108
|
+
? today.year + 1
|
|
109
|
+
: today.year
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
// Figure out which days are allowed
|
|
113
|
+
const days = [];
|
|
114
|
+
const numDaysInMonth = (new Date(year, month, 0)).getDate();
|
|
115
|
+
const firstDay = (
|
|
116
|
+
month === today.month
|
|
117
|
+
? today.day // Current month: start at current date
|
|
118
|
+
: 1 // Future month: start at beginning of month
|
|
119
|
+
);
|
|
120
|
+
for (let day = firstDay; day <= numDaysInMonth; day++) {
|
|
121
|
+
days.push(day);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
choices.push({
|
|
125
|
+
choiceName: `${monthName} ${year}`,
|
|
126
|
+
month,
|
|
127
|
+
year,
|
|
128
|
+
days,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Create choice options
|
|
133
|
+
const monthOptions: React.ReactNode[] = [];
|
|
134
|
+
const dayOptions: React.ReactNode[] = [];
|
|
135
|
+
choices.forEach((choice) => {
|
|
136
|
+
// Create month option
|
|
137
|
+
monthOptions.push(
|
|
138
|
+
<option
|
|
139
|
+
key={choice.month}
|
|
140
|
+
value={choice.month}
|
|
141
|
+
aria-label={`choose ${choice.choiceName}`}
|
|
142
|
+
onSelect={() => {
|
|
143
|
+
onChange(choice.month, choice.days[0], choice.year);
|
|
144
|
+
}}
|
|
145
|
+
>
|
|
146
|
+
{choice.choiceName}
|
|
147
|
+
</option>
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
if (month === choice.month) {
|
|
151
|
+
// This is the currently selected month
|
|
152
|
+
// Create day options
|
|
153
|
+
choice.days.forEach((dayChoice) => {
|
|
154
|
+
const ordinal = getOrdinal(dayChoice);
|
|
155
|
+
dayOptions.push(
|
|
156
|
+
<option
|
|
157
|
+
key={dayChoice}
|
|
158
|
+
value={dayChoice}
|
|
159
|
+
aria-label={`choose date ${dayChoice}`}
|
|
160
|
+
>
|
|
161
|
+
{dayChoice}
|
|
162
|
+
{ordinal}
|
|
163
|
+
</option>
|
|
164
|
+
);
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
return (
|
|
170
|
+
<div
|
|
171
|
+
className="SimpleDateChooser d-inline-block"
|
|
172
|
+
aria-label={`date chooser with selected date: ${month} ${day}, ${year}`}
|
|
173
|
+
>
|
|
174
|
+
{/* Month Chooser */}
|
|
175
|
+
<select
|
|
176
|
+
aria-label={`month for ${ariaLabel}`}
|
|
177
|
+
className="custom-select d-inline-block mr-1"
|
|
178
|
+
style={{ width: 'auto' }}
|
|
179
|
+
id={`SimpleDateChooser-${name}-month`}
|
|
180
|
+
value={month}
|
|
181
|
+
onChange={(e) => {
|
|
182
|
+
const choice = choices[e.target.selectedIndex];
|
|
183
|
+
|
|
184
|
+
// Change day, month, and year
|
|
185
|
+
onChange(
|
|
186
|
+
choice.month,
|
|
187
|
+
choice.days[0],
|
|
188
|
+
choice.year,
|
|
189
|
+
);
|
|
190
|
+
}}
|
|
191
|
+
>
|
|
192
|
+
{monthOptions}
|
|
193
|
+
</select>
|
|
194
|
+
|
|
195
|
+
{/* Day Chooser */}
|
|
196
|
+
<select
|
|
197
|
+
aria-label={`day for ${ariaLabel}`}
|
|
198
|
+
className="custom-select d-inline-block"
|
|
199
|
+
style={{ width: 'auto' }}
|
|
200
|
+
id={`SimpleDateChooser-${name}-day`}
|
|
201
|
+
value={day}
|
|
202
|
+
onChange={(e) => {
|
|
203
|
+
// Only change the day
|
|
204
|
+
onChange(
|
|
205
|
+
month,
|
|
206
|
+
Number.parseInt(e.target.value, 10),
|
|
207
|
+
year,
|
|
208
|
+
);
|
|
209
|
+
}}
|
|
210
|
+
>
|
|
211
|
+
{dayOptions}
|
|
212
|
+
</select>
|
|
213
|
+
</div>
|
|
214
|
+
);
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
/*------------------------------------------------------------------------*/
|
|
218
|
+
/* Wrap Up */
|
|
219
|
+
/*------------------------------------------------------------------------*/
|
|
220
|
+
|
|
221
|
+
// Export component
|
|
222
|
+
export default SimpleDateChooser;
|