dce-reactkit 3.0.0-beta.1 → 3.0.0-beta.12

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.
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * A wrapper for the entire React app that adds global functionality like
3
- * handling for fatal error messages
3
+ * handling for fatal error messages, adds bootstrap support
4
4
  * @author Gabe Abrams
5
5
  */
6
6
  import React from 'react';
package/dist/index.d.ts CHANGED
@@ -3,7 +3,7 @@ import React from 'react';
3
3
 
4
4
  /**
5
5
  * A wrapper for the entire React app that adds global functionality like
6
- * handling for fatal error messages
6
+ * handling for fatal error messages, adds bootstrap support
7
7
  * @author Gabe Abrams
8
8
  */
9
9
 
package/package.json CHANGED
@@ -1,12 +1,9 @@
1
1
  {
2
2
  "name": "dce-reactkit",
3
- "version": "3.0.0-beta.1",
3
+ "version": "3.0.0-beta.12",
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
  }
@@ -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;