dce-reactkit 3.0.0-beta.1 → 3.0.0-beta.10
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/.eslintrc.js +95 -0
- package/dist/cjs/index.js +140 -35643
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/types/components/AppWrapper.d.ts +1 -1
- package/dist/esm/index.js +43 -35528
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/types/components/AppWrapper.d.ts +1 -1
- package/dist/index.d.ts +1 -1
- package/package.json +14 -7
- package/rollup.config.js +45 -0
- package/src/components/AppWrapper.tsx +370 -0
- package/src/components/ErrorBox.tsx +124 -0
- package/src/components/LoadingSpinner.tsx +108 -0
- package/src/components/Modal.tsx +374 -0
- package/src/components/TabBox.tsx +136 -0
- package/src/errors/ErrorWithCode.tsx +15 -0
- package/src/helpers/abbreviate.tsx +25 -0
- package/src/helpers/avg.tsx +22 -0
- package/src/helpers/ceilToNumDecimals.tsx +13 -0
- package/src/helpers/floorToNumDecimals.tsx +13 -0
- package/src/helpers/forceNumIntoBounds.tsx +19 -0
- package/src/helpers/padDecimalZeros.tsx +32 -0
- package/src/helpers/padZerosLeft.tsx +21 -0
- package/src/helpers/roundToNumDecimals.tsx +13 -0
- package/src/helpers/sum.tsx +16 -0
- package/src/helpers/waitMs.tsx +12 -0
- package/src/index.ts +61 -0
- package/src/types/ModalButtonType.tsx +18 -0
- package/src/types/ModalSize.tsx +12 -0
- package/src/types/ModalType.tsx +17 -0
- package/src/types/ReactKitErrorCode.tsx +16 -0
- package/src/types/Variant.tsx +16 -0
- package/tsconfig.json +23 -0
package/dist/index.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dce-reactkit",
|
|
3
|
-
"version": "3.0.0-beta.
|
|
3
|
+
"version": "3.0.0-beta.10",
|
|
4
4
|
"description": "Shared components for Harvard DCE apps",
|
|
5
5
|
"main": "dist/cjs/index.js",
|
|
6
6
|
"module": "dist/esm/index.js",
|
|
7
|
-
"files": [
|
|
8
|
-
"dist"
|
|
9
|
-
],
|
|
10
7
|
"types": "dist/index.d.ts",
|
|
11
8
|
"scripts": {
|
|
12
9
|
"build": "rm -rf dist && rollup -c"
|
|
@@ -22,18 +19,28 @@
|
|
|
22
19
|
},
|
|
23
20
|
"homepage": "https://github.com/harvard-edtech/dce-reactkit#readme",
|
|
24
21
|
"peerDependencies": {
|
|
25
|
-
"react": "^18.0.0",
|
|
26
|
-
"react-bootstrap": "^2.3.0",
|
|
27
22
|
"@fortawesome/free-solid-svg-icons": "^6.1.1",
|
|
28
|
-
"@fortawesome/react-fontawesome": "^0.1.18"
|
|
23
|
+
"@fortawesome/react-fontawesome": "^0.1.18",
|
|
24
|
+
"bootstrap": "^5.1.3",
|
|
25
|
+
"react": "^18.0.0"
|
|
29
26
|
},
|
|
30
27
|
"devDependencies": {
|
|
31
28
|
"@rollup/plugin-commonjs": "^22.0.0",
|
|
32
29
|
"@rollup/plugin-node-resolve": "^13.2.1",
|
|
33
30
|
"@rollup/plugin-typescript": "^8.3.2",
|
|
34
31
|
"@types/react": "^18.0.7",
|
|
32
|
+
"@typescript-eslint/eslint-plugin": "^5.21.0",
|
|
33
|
+
"@typescript-eslint/parser": "^5.21.0",
|
|
34
|
+
"eslint": "^8.14.0",
|
|
35
|
+
"eslint-config-airbnb": "^19.0.4",
|
|
36
|
+
"eslint-config-airbnb-typescript": "^17.0.0",
|
|
37
|
+
"eslint-plugin-import": "^2.26.0",
|
|
38
|
+
"eslint-plugin-jsx-a11y": "^6.5.1",
|
|
39
|
+
"eslint-plugin-react": "^7.29.4",
|
|
40
|
+
"eslint-plugin-react-hooks": "^4.4.0",
|
|
35
41
|
"rollup": "^2.70.2",
|
|
36
42
|
"rollup-plugin-dts": "^4.2.1",
|
|
43
|
+
"rollup-plugin-sourcemaps": "^0.6.3",
|
|
37
44
|
"typescript": "^4.6.3"
|
|
38
45
|
}
|
|
39
46
|
}
|
package/rollup.config.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import resolve from "@rollup/plugin-node-resolve";
|
|
2
|
+
import commonjs from "@rollup/plugin-commonjs";
|
|
3
|
+
import typescript from "@rollup/plugin-typescript";
|
|
4
|
+
import dts from "rollup-plugin-dts";
|
|
5
|
+
import sourcemaps from 'rollup-plugin-sourcemaps';
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
const packageJson = require("./package.json");
|
|
9
|
+
|
|
10
|
+
export default [
|
|
11
|
+
{
|
|
12
|
+
input: "src/index.ts",
|
|
13
|
+
output: [
|
|
14
|
+
{
|
|
15
|
+
file: packageJson.main,
|
|
16
|
+
format: "cjs",
|
|
17
|
+
sourcemap: true,
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
file: packageJson.module,
|
|
21
|
+
format: "esm",
|
|
22
|
+
sourcemap: true,
|
|
23
|
+
},
|
|
24
|
+
],
|
|
25
|
+
plugins: [
|
|
26
|
+
resolve(),
|
|
27
|
+
commonjs(),
|
|
28
|
+
typescript({ tsconfig: "./tsconfig.json" }),
|
|
29
|
+
sourcemaps(),
|
|
30
|
+
],
|
|
31
|
+
external: [
|
|
32
|
+
...Object.keys(packageJson.dependencies || {}),
|
|
33
|
+
...Object.keys(packageJson.peerDependencies || {}),
|
|
34
|
+
],
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
input: "dist/esm/types/index.d.ts",
|
|
38
|
+
output: [{ file: "dist/index.d.ts", format: "esm" }],
|
|
39
|
+
plugins: [dts()],
|
|
40
|
+
external: [
|
|
41
|
+
...Object.keys(packageJson.dependencies || {}),
|
|
42
|
+
...Object.keys(packageJson.peerDependencies || {}),
|
|
43
|
+
],
|
|
44
|
+
},
|
|
45
|
+
];
|
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A wrapper for the entire React app that adds global functionality like
|
|
3
|
+
* handling for fatal error messages, adds bootstrap support
|
|
4
|
+
* @author Gabe Abrams
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// Import React
|
|
8
|
+
import React, { useState } from 'react';
|
|
9
|
+
|
|
10
|
+
// Import shared components
|
|
11
|
+
import ErrorBox from './ErrorBox';
|
|
12
|
+
|
|
13
|
+
// Import shared types
|
|
14
|
+
import ReactKitErrorCode from '../types/ReactKitErrorCode';
|
|
15
|
+
import ModalButtonType from '../types/ModalButtonType';
|
|
16
|
+
import ModalType from '../types/ModalType';
|
|
17
|
+
|
|
18
|
+
// Import shared components
|
|
19
|
+
import Modal from './Modal';
|
|
20
|
+
|
|
21
|
+
// Import custom errors
|
|
22
|
+
import ErrorWithCode from '../errors/ErrorWithCode';
|
|
23
|
+
|
|
24
|
+
/*------------------------------------------------------------------------*/
|
|
25
|
+
/* Props */
|
|
26
|
+
/*------------------------------------------------------------------------*/
|
|
27
|
+
|
|
28
|
+
type Props = {
|
|
29
|
+
// The entire app
|
|
30
|
+
children: React.ReactNode,
|
|
31
|
+
// True if this app is a dark-themed app
|
|
32
|
+
dark?: boolean,
|
|
33
|
+
// Custom session expired message
|
|
34
|
+
sessionExpiredMessage?: string,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/*------------------------------------------------------------------------*/
|
|
38
|
+
/* Static Helpers */
|
|
39
|
+
/*------------------------------------------------------------------------*/
|
|
40
|
+
|
|
41
|
+
/*----------------------------------------*/
|
|
42
|
+
/* Alert */
|
|
43
|
+
/*----------------------------------------*/
|
|
44
|
+
|
|
45
|
+
// Stored copies of setters
|
|
46
|
+
let setAlertInfo: (info: { title: string, text: string }) => void;
|
|
47
|
+
let onAlertClosed: () => void;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Show an alert modal with an "Okay" button
|
|
51
|
+
* @author Gabe Abrams
|
|
52
|
+
* @param title the title text to display at the top of the alert
|
|
53
|
+
* @param text the text to display in the alert
|
|
54
|
+
*/
|
|
55
|
+
export const alert = async (title: string, text: string): Promise<undefined> => {
|
|
56
|
+
// Fallback if alert not available
|
|
57
|
+
if (!setAlertInfo) {
|
|
58
|
+
window.alert(`${title}\n\n${text}`);
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Return promise that resolves when alert is closed
|
|
63
|
+
return new Promise((resolve) => {
|
|
64
|
+
// Setup handler
|
|
65
|
+
onAlertClosed = () => {
|
|
66
|
+
resolve(undefined);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
// Show the alert
|
|
70
|
+
setAlertInfo({
|
|
71
|
+
title,
|
|
72
|
+
text,
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/*----------------------------------------*/
|
|
78
|
+
/* Confirm */
|
|
79
|
+
/*----------------------------------------*/
|
|
80
|
+
|
|
81
|
+
// Stored copies of setters
|
|
82
|
+
let setConfirmInfo: (info: { title: string, text: string }) => void;
|
|
83
|
+
let onConfirmClosed: (confirmed: boolean) => void;
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Show a confirmation modal with an "Okay" and a "Cancel" button
|
|
87
|
+
* @author Gabe Abrams
|
|
88
|
+
* @param title the title text to display at the top of the alert
|
|
89
|
+
* @param text the text to display in the alert
|
|
90
|
+
* @returns true if the user confirmed
|
|
91
|
+
*/
|
|
92
|
+
export const confirm = async (
|
|
93
|
+
title: string,
|
|
94
|
+
text: string,
|
|
95
|
+
): Promise<boolean> => {
|
|
96
|
+
// Fallback if confirm is not available
|
|
97
|
+
if (!setConfirmInfo) {
|
|
98
|
+
return window.confirm(`${title}\n\n${text}`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Return promise that resolves with result of confirmation
|
|
102
|
+
return new Promise((resolve) => {
|
|
103
|
+
// Setup handler
|
|
104
|
+
onConfirmClosed = (confirmed: boolean) => {
|
|
105
|
+
resolve(confirmed)
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
// Show the confirm
|
|
109
|
+
setConfirmInfo({
|
|
110
|
+
title,
|
|
111
|
+
text,
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
/*----------------------------------------*/
|
|
117
|
+
/* Fatal Error */
|
|
118
|
+
/*----------------------------------------*/
|
|
119
|
+
|
|
120
|
+
// Stored copies of setters
|
|
121
|
+
let setFatalErrorMessage: (message: string) => void;
|
|
122
|
+
let setFatalErrorCode: (code: string) => void;
|
|
123
|
+
let setFatalErrorTitle: (title: string) => void;
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Show a fatal error message
|
|
127
|
+
* @author Gabe Abrams
|
|
128
|
+
* @param error the error to show
|
|
129
|
+
* @param [errorTitle] title of the error box
|
|
130
|
+
*/
|
|
131
|
+
export const showFatalError = (
|
|
132
|
+
error: any,
|
|
133
|
+
errorTitle: string = 'An Error Occurred',
|
|
134
|
+
): undefined => {
|
|
135
|
+
// Determine message and code
|
|
136
|
+
const message: string = (
|
|
137
|
+
typeof error === 'string'
|
|
138
|
+
? error.trim()
|
|
139
|
+
: String(error.message ?? 'An unknown error occurred.')
|
|
140
|
+
);
|
|
141
|
+
const code: string = (
|
|
142
|
+
typeof error === 'string'
|
|
143
|
+
? ReactKitErrorCode.NoCode
|
|
144
|
+
: String(error.code ?? ReactKitErrorCode.NoCode)
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
// Handle case where app hasn't loaded
|
|
148
|
+
if (!setFatalErrorMessage || !setFatalErrorCode) {
|
|
149
|
+
alert(
|
|
150
|
+
errorTitle,
|
|
151
|
+
`${message} (code: ${code}). Please contact support.`,
|
|
152
|
+
);
|
|
153
|
+
return undefined;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Use setters
|
|
157
|
+
setFatalErrorMessage(message);
|
|
158
|
+
setFatalErrorCode(code);
|
|
159
|
+
setFatalErrorTitle(errorTitle);
|
|
160
|
+
|
|
161
|
+
return undefined;
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
/*----------------------------------------*/
|
|
165
|
+
/* Session Expired */
|
|
166
|
+
/*----------------------------------------*/
|
|
167
|
+
|
|
168
|
+
// Stored copies of setters
|
|
169
|
+
let setSessionHasExpired: (isExpired: boolean) => void;
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Show the "session expired" message
|
|
173
|
+
* @author Gabe Abrams
|
|
174
|
+
*/
|
|
175
|
+
export const showSessionExpiredMessage = (): undefined => {
|
|
176
|
+
setSessionHasExpired(true);
|
|
177
|
+
return undefined;
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
/*------------------------------------------------------------------------*/
|
|
181
|
+
/* Component */
|
|
182
|
+
/*------------------------------------------------------------------------*/
|
|
183
|
+
|
|
184
|
+
const AppWrapper: React.FC<Props> = (props: Props): React.ReactElement => {
|
|
185
|
+
/*------------------------------------------------------------------------*/
|
|
186
|
+
/* Setup */
|
|
187
|
+
/*------------------------------------------------------------------------*/
|
|
188
|
+
|
|
189
|
+
/* -------------- Props ------------- */
|
|
190
|
+
|
|
191
|
+
const {
|
|
192
|
+
children,
|
|
193
|
+
dark,
|
|
194
|
+
sessionExpiredMessage = 'Your session has expired. Please go back to Canvas and start over.',
|
|
195
|
+
} = props;
|
|
196
|
+
|
|
197
|
+
/* -------------- State ------------- */
|
|
198
|
+
|
|
199
|
+
// Fatal error
|
|
200
|
+
const [
|
|
201
|
+
fatalErrorMessage,
|
|
202
|
+
setFatalErrorMessageInner,
|
|
203
|
+
] = useState<string | undefined>();
|
|
204
|
+
setFatalErrorMessage = setFatalErrorMessageInner;
|
|
205
|
+
const [
|
|
206
|
+
fatalErrorCode,
|
|
207
|
+
setFatalErrorCodeInner,
|
|
208
|
+
] = useState<string | undefined>();
|
|
209
|
+
setFatalErrorCode = setFatalErrorCodeInner;
|
|
210
|
+
const [
|
|
211
|
+
fatalErrorTitle,
|
|
212
|
+
setFatalErrorTitleInner,
|
|
213
|
+
] = useState<string | undefined>();
|
|
214
|
+
setFatalErrorTitle = setFatalErrorTitleInner;
|
|
215
|
+
|
|
216
|
+
// Alert
|
|
217
|
+
const [
|
|
218
|
+
alertInfo,
|
|
219
|
+
setAlertInfoInner,
|
|
220
|
+
] = useState<{
|
|
221
|
+
title: string,
|
|
222
|
+
text: string
|
|
223
|
+
}>();
|
|
224
|
+
setAlertInfo = setAlertInfoInner;
|
|
225
|
+
|
|
226
|
+
// Confirm
|
|
227
|
+
const [
|
|
228
|
+
confirmInfo,
|
|
229
|
+
setConfirmInfoInner,
|
|
230
|
+
] = useState<{
|
|
231
|
+
title: string,
|
|
232
|
+
text: string
|
|
233
|
+
}>();
|
|
234
|
+
setConfirmInfo = setConfirmInfoInner;
|
|
235
|
+
|
|
236
|
+
// Session expired
|
|
237
|
+
const [
|
|
238
|
+
sessionHasExpired,
|
|
239
|
+
setSessionHasExpiredInner,
|
|
240
|
+
] = useState<boolean>(false);
|
|
241
|
+
setSessionHasExpired = setSessionHasExpiredInner;
|
|
242
|
+
|
|
243
|
+
/*------------------------------------------------------------------------*/
|
|
244
|
+
/* Render */
|
|
245
|
+
/*------------------------------------------------------------------------*/
|
|
246
|
+
|
|
247
|
+
/*----------------------------------------*/
|
|
248
|
+
/* Modal */
|
|
249
|
+
/*----------------------------------------*/
|
|
250
|
+
|
|
251
|
+
// Modal that may be defined
|
|
252
|
+
let modal: React.ReactNode;
|
|
253
|
+
|
|
254
|
+
/* -------------- Alert ------------- */
|
|
255
|
+
|
|
256
|
+
if (alertInfo) {
|
|
257
|
+
modal = (
|
|
258
|
+
<Modal
|
|
259
|
+
title={alertInfo.title}
|
|
260
|
+
type={ModalType.Okay}
|
|
261
|
+
onClose={() => {
|
|
262
|
+
// Alert closed
|
|
263
|
+
if (onAlertClosed) {
|
|
264
|
+
onAlertClosed();
|
|
265
|
+
}
|
|
266
|
+
}}
|
|
267
|
+
onTopOfOtherModals
|
|
268
|
+
>
|
|
269
|
+
{alertInfo.text}
|
|
270
|
+
</Modal>
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/* ------------- Confirm ------------ */
|
|
275
|
+
|
|
276
|
+
if (confirmInfo) {
|
|
277
|
+
modal = (
|
|
278
|
+
<Modal
|
|
279
|
+
title={confirmInfo.title}
|
|
280
|
+
type={ModalType.OkayCancel}
|
|
281
|
+
onClose={(buttonType) => {
|
|
282
|
+
if (onConfirmClosed) {
|
|
283
|
+
onConfirmClosed(buttonType === ModalButtonType.Okay);
|
|
284
|
+
}
|
|
285
|
+
}}
|
|
286
|
+
dontAllowBackdropExit
|
|
287
|
+
>
|
|
288
|
+
|
|
289
|
+
</Modal>
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/*----------------------------------------*/
|
|
294
|
+
/* Views */
|
|
295
|
+
/*----------------------------------------*/
|
|
296
|
+
|
|
297
|
+
// Body that will be filled with the current view
|
|
298
|
+
let body: React.ReactNode;
|
|
299
|
+
|
|
300
|
+
/* ----------- Fatal Error ---------- */
|
|
301
|
+
|
|
302
|
+
if (fatalErrorMessage || fatalErrorCode || sessionHasExpired) {
|
|
303
|
+
// Re-encapsulate in an error
|
|
304
|
+
const error = (
|
|
305
|
+
sessionHasExpired
|
|
306
|
+
? new ErrorWithCode(
|
|
307
|
+
(fatalErrorMessage ?? 'An unknown error has occurred. Please contact support.'),
|
|
308
|
+
(fatalErrorCode ?? ReactKitErrorCode.NoCode),
|
|
309
|
+
)
|
|
310
|
+
: new ErrorWithCode(
|
|
311
|
+
sessionExpiredMessage,
|
|
312
|
+
ReactKitErrorCode.SessionExpired,
|
|
313
|
+
)
|
|
314
|
+
);
|
|
315
|
+
|
|
316
|
+
// Build error screen
|
|
317
|
+
body = (
|
|
318
|
+
<div
|
|
319
|
+
style={{
|
|
320
|
+
display: 'block',
|
|
321
|
+
width: '100vw',
|
|
322
|
+
minHeight: '100vh',
|
|
323
|
+
paddingTop: '2rem',
|
|
324
|
+
backgroundColor: (
|
|
325
|
+
dark
|
|
326
|
+
? '#222'
|
|
327
|
+
: '#fff'
|
|
328
|
+
),
|
|
329
|
+
}}
|
|
330
|
+
>
|
|
331
|
+
<ErrorBox
|
|
332
|
+
title={(
|
|
333
|
+
sessionHasExpired
|
|
334
|
+
? 'Session Expired'
|
|
335
|
+
: fatalErrorTitle
|
|
336
|
+
)}
|
|
337
|
+
error={error}
|
|
338
|
+
/>
|
|
339
|
+
</div>
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/* --------------- App -------------- */
|
|
344
|
+
|
|
345
|
+
if (!body) {
|
|
346
|
+
body = (
|
|
347
|
+
<>
|
|
348
|
+
{children}
|
|
349
|
+
</>
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/*----------------------------------------*/
|
|
354
|
+
/* Main UI */
|
|
355
|
+
/*----------------------------------------*/
|
|
356
|
+
|
|
357
|
+
return (
|
|
358
|
+
<>
|
|
359
|
+
{modal}
|
|
360
|
+
{body}
|
|
361
|
+
</>
|
|
362
|
+
);
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
/*------------------------------------------------------------------------*/
|
|
366
|
+
/* Wrap Up */
|
|
367
|
+
/*------------------------------------------------------------------------*/
|
|
368
|
+
|
|
369
|
+
// Export component
|
|
370
|
+
export default AppWrapper;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Displays an error
|
|
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 { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons';
|
|
12
|
+
|
|
13
|
+
// Import shared types
|
|
14
|
+
import ReactKitErrorCode from '../types/ReactKitErrorCode';
|
|
15
|
+
|
|
16
|
+
/*------------------------------------------------------------------------*/
|
|
17
|
+
/* Types */
|
|
18
|
+
/*------------------------------------------------------------------------*/
|
|
19
|
+
|
|
20
|
+
type Props = {
|
|
21
|
+
// Error to display
|
|
22
|
+
error: any,
|
|
23
|
+
// Customized title of error box
|
|
24
|
+
title?: string,
|
|
25
|
+
// Handler for close,
|
|
26
|
+
onClose?: () => void,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/*------------------------------------------------------------------------*/
|
|
30
|
+
/* Component */
|
|
31
|
+
/*------------------------------------------------------------------------*/
|
|
32
|
+
|
|
33
|
+
const ErrorBox: React.FC<Props> = (props) => {
|
|
34
|
+
/*------------------------------------------------------------------------*/
|
|
35
|
+
/* Setup */
|
|
36
|
+
/*------------------------------------------------------------------------*/
|
|
37
|
+
|
|
38
|
+
/* -------------- Props ------------- */
|
|
39
|
+
|
|
40
|
+
const {
|
|
41
|
+
error,
|
|
42
|
+
title = 'An Error Occurred',
|
|
43
|
+
onClose,
|
|
44
|
+
} = props;
|
|
45
|
+
|
|
46
|
+
/*------------------------------------------------------------------------*/
|
|
47
|
+
/* Render */
|
|
48
|
+
/*------------------------------------------------------------------------*/
|
|
49
|
+
|
|
50
|
+
// Determine error text
|
|
51
|
+
const errorText: string = (
|
|
52
|
+
typeof error === 'string'
|
|
53
|
+
? error.trim()
|
|
54
|
+
: String(error.message || 'An unknown error occurred. Please contact support.')
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
// Error code box
|
|
58
|
+
let errorCodeBox;
|
|
59
|
+
if (error && (error as any).code) {
|
|
60
|
+
errorCodeBox = (
|
|
61
|
+
<span>
|
|
62
|
+
{' '}
|
|
63
|
+
<span
|
|
64
|
+
style={{
|
|
65
|
+
backgroundColor: 'white',
|
|
66
|
+
borderRadius: '5px',
|
|
67
|
+
paddingLeft: '3px',
|
|
68
|
+
paddingRight: '3px',
|
|
69
|
+
color: '#DC4150',
|
|
70
|
+
fontVariant: 'small-caps',
|
|
71
|
+
fontSize: '80%',
|
|
72
|
+
whiteSpace: 'nowrap',
|
|
73
|
+
}}
|
|
74
|
+
>
|
|
75
|
+
code:
|
|
76
|
+
{' '}
|
|
77
|
+
{String((error as any).code ?? ReactKitErrorCode.NoCode).toUpperCase()}
|
|
78
|
+
</span>
|
|
79
|
+
</span>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Main UI
|
|
84
|
+
return (
|
|
85
|
+
<div
|
|
86
|
+
className="alert alert-danger text-center"
|
|
87
|
+
style={{
|
|
88
|
+
maxWidth: '650px',
|
|
89
|
+
margin: 'auto',
|
|
90
|
+
}}
|
|
91
|
+
>
|
|
92
|
+
<h4 className="mb-1">
|
|
93
|
+
<FontAwesomeIcon
|
|
94
|
+
icon={faExclamationTriangle}
|
|
95
|
+
className="me-2"
|
|
96
|
+
/>
|
|
97
|
+
{title}
|
|
98
|
+
</h4>
|
|
99
|
+
<div>
|
|
100
|
+
{errorText}
|
|
101
|
+
{errorCodeBox}
|
|
102
|
+
</div>
|
|
103
|
+
{onClose && (
|
|
104
|
+
<div className="mt-2">
|
|
105
|
+
<button
|
|
106
|
+
type="button"
|
|
107
|
+
className="btn btn-light"
|
|
108
|
+
aria-label="dismiss error and close this activity or view"
|
|
109
|
+
onClick={onClose}
|
|
110
|
+
>
|
|
111
|
+
Close
|
|
112
|
+
</button>
|
|
113
|
+
</div>
|
|
114
|
+
)}
|
|
115
|
+
</div>
|
|
116
|
+
);
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
/*------------------------------------------------------------------------*/
|
|
120
|
+
/* Wrap Up */
|
|
121
|
+
/*------------------------------------------------------------------------*/
|
|
122
|
+
|
|
123
|
+
// Export component
|
|
124
|
+
export default ErrorBox;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Loading spinner/indicator
|
|
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 { faCircle } from '@fortawesome/free-solid-svg-icons';
|
|
12
|
+
|
|
13
|
+
/*------------------------------------------------------------------------*/
|
|
14
|
+
/* Style */
|
|
15
|
+
/*------------------------------------------------------------------------*/
|
|
16
|
+
|
|
17
|
+
const style = `
|
|
18
|
+
/* Blips */
|
|
19
|
+
.LoadingSpinner-blip-1,
|
|
20
|
+
.LoadingSpinner-blip-2,
|
|
21
|
+
.LoadingSpinner-blip-3,
|
|
22
|
+
.LoadingSpinner-blip-4 {
|
|
23
|
+
font-size: 25px;
|
|
24
|
+
opacity: 0.6;
|
|
25
|
+
margin-top: 20px;
|
|
26
|
+
margin-bottom: 20px;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/* First Blip */
|
|
30
|
+
.LoadingSpinner-blip-1 {
|
|
31
|
+
animation: LoadingSpinner-pop-animation 2s infinite;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/* Second Blip */
|
|
35
|
+
.LoadingSpinner-blip-2 {
|
|
36
|
+
animation: LoadingSpinner-pop-animation 2s infinite;
|
|
37
|
+
-webkit-animation-delay: 0.1s;
|
|
38
|
+
animation-delay: 0.1s;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/* Third Blip */
|
|
42
|
+
.LoadingSpinner-blip-3 {
|
|
43
|
+
animation: LoadingSpinner-pop-animation 2s infinite;
|
|
44
|
+
animation-delay: 0.2s;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/* Fourth Blip */
|
|
48
|
+
.LoadingSpinner-blip-4 {
|
|
49
|
+
animation: LoadingSpinner-pop-animation 2s infinite;
|
|
50
|
+
animation-delay: 0.3s;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/* Pop Animation for Each Blip */
|
|
54
|
+
@keyframes LoadingSpinner-pop-animation {
|
|
55
|
+
0% {
|
|
56
|
+
transform: scale(1.0);
|
|
57
|
+
}
|
|
58
|
+
10% {
|
|
59
|
+
transform: scale(1.5);
|
|
60
|
+
}
|
|
61
|
+
30% {
|
|
62
|
+
transform: scale(1.0);
|
|
63
|
+
}
|
|
64
|
+
100% {
|
|
65
|
+
transform: scale(1.0);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
`;
|
|
69
|
+
|
|
70
|
+
/*------------------------------------------------------------------------*/
|
|
71
|
+
/* Component */
|
|
72
|
+
/*------------------------------------------------------------------------*/
|
|
73
|
+
|
|
74
|
+
const LoadingSpinner = () => {
|
|
75
|
+
/*------------------------------------------------------------------------*/
|
|
76
|
+
/* Render */
|
|
77
|
+
/*------------------------------------------------------------------------*/
|
|
78
|
+
|
|
79
|
+
// Add all four blips to a container
|
|
80
|
+
return (
|
|
81
|
+
<div className="text-center LoadingSpinner LoadingSpinner-container">
|
|
82
|
+
<style>{style}</style>
|
|
83
|
+
<FontAwesomeIcon
|
|
84
|
+
icon={faCircle}
|
|
85
|
+
className="LoadingSpinner-blip-1 me-1"
|
|
86
|
+
/>
|
|
87
|
+
<FontAwesomeIcon
|
|
88
|
+
icon={faCircle}
|
|
89
|
+
className="LoadingSpinner-blip-2 me-1"
|
|
90
|
+
/>
|
|
91
|
+
<FontAwesomeIcon
|
|
92
|
+
icon={faCircle}
|
|
93
|
+
className="LoadingSpinner-blip-3 me-1"
|
|
94
|
+
/>
|
|
95
|
+
<FontAwesomeIcon
|
|
96
|
+
icon={faCircle}
|
|
97
|
+
className="LoadingSpinner-blip-4"
|
|
98
|
+
/>
|
|
99
|
+
</div>
|
|
100
|
+
);
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
/*------------------------------------------------------------------------*/
|
|
104
|
+
/* Wrap Up */
|
|
105
|
+
/*------------------------------------------------------------------------*/
|
|
106
|
+
|
|
107
|
+
// Export component
|
|
108
|
+
export default LoadingSpinner;
|