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

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,9 +1,11 @@
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';
7
+ import 'bootstrap/dist/css/bootstrap.min.css';
8
+ import 'bootstrap/dist/js/bootstrap.min';
7
9
  declare type Props = {
8
10
  children: React.ReactNode;
9
11
  dark?: boolean;
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.4",
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,10 +19,9 @@
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
+ "react": "^18.0.0"
29
25
  },
30
26
  "devDependencies": {
31
27
  "@rollup/plugin-commonjs": "^22.0.0",
@@ -34,6 +30,7 @@
34
30
  "@types/react": "^18.0.7",
35
31
  "rollup": "^2.70.2",
36
32
  "rollup-plugin-dts": "^4.2.1",
33
+ "rollup-plugin-sourcemaps": "^0.6.3",
37
34
  "typescript": "^4.6.3"
38
35
  }
39
36
  }
@@ -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,374 @@
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 bootstrap stylesheet and javascript
11
+ import 'bootstrap/dist/css/bootstrap.min.css';
12
+ import 'bootstrap/dist/js/bootstrap.min';
13
+
14
+ // Import shared components
15
+ import ErrorBox from './ErrorBox';
16
+
17
+ // Import shared types
18
+ import ReactKitErrorCode from '../types/ReactKitErrorCode';
19
+ import ModalButtonType from '../types/ModalButtonType';
20
+ import ModalType from '../types/ModalType';
21
+
22
+ // Import shared components
23
+ import Modal from './Modal';
24
+
25
+ // Import custom errors
26
+ import ErrorWithCode from '../errors/ErrorWithCode';
27
+
28
+ /*------------------------------------------------------------------------*/
29
+ /* Props */
30
+ /*------------------------------------------------------------------------*/
31
+
32
+ type Props = {
33
+ // The entire app
34
+ children: React.ReactNode,
35
+ // True if this app is a dark-themed app
36
+ dark?: boolean,
37
+ // Custom session expired message
38
+ sessionExpiredMessage?: string,
39
+ };
40
+
41
+ /*------------------------------------------------------------------------*/
42
+ /* Static Helpers */
43
+ /*------------------------------------------------------------------------*/
44
+
45
+ /*----------------------------------------*/
46
+ /* Alert */
47
+ /*----------------------------------------*/
48
+
49
+ // Stored copies of setters
50
+ let setAlertInfo: (info: { title: string, text: string }) => void;
51
+ let onAlertClosed: () => void;
52
+
53
+ /**
54
+ * Show an alert modal with an "Okay" button
55
+ * @author Gabe Abrams
56
+ * @param title the title text to display at the top of the alert
57
+ * @param text the text to display in the alert
58
+ */
59
+ export const alert = async (title: string, text: string): Promise<undefined> => {
60
+ // Fallback if alert not available
61
+ if (!setAlertInfo) {
62
+ window.alert(`${title}\n\n${text}`);
63
+ return undefined;
64
+ }
65
+
66
+ // Return promise that resolves when alert is closed
67
+ return new Promise((resolve) => {
68
+ // Setup handler
69
+ onAlertClosed = () => {
70
+ resolve(undefined);
71
+ };
72
+
73
+ // Show the alert
74
+ setAlertInfo({
75
+ title,
76
+ text,
77
+ });
78
+ });
79
+ };
80
+
81
+ /*----------------------------------------*/
82
+ /* Confirm */
83
+ /*----------------------------------------*/
84
+
85
+ // Stored copies of setters
86
+ let setConfirmInfo: (info: { title: string, text: string }) => void;
87
+ let onConfirmClosed: (confirmed: boolean) => void;
88
+
89
+ /**
90
+ * Show a confirmation modal with an "Okay" and a "Cancel" button
91
+ * @author Gabe Abrams
92
+ * @param title the title text to display at the top of the alert
93
+ * @param text the text to display in the alert
94
+ * @returns true if the user confirmed
95
+ */
96
+ export const confirm = async (
97
+ title: string,
98
+ text: string,
99
+ ): Promise<boolean> => {
100
+ // Fallback if confirm is not available
101
+ if (!setConfirmInfo) {
102
+ return window.confirm(`${title}\n\n${text}`);
103
+ }
104
+
105
+ // Return promise that resolves with result of confirmation
106
+ return new Promise((resolve) => {
107
+ // Setup handler
108
+ onConfirmClosed = (confirmed: boolean) => {
109
+ resolve(confirmed)
110
+ };
111
+
112
+ // Show the confirm
113
+ setConfirmInfo({
114
+ title,
115
+ text,
116
+ });
117
+ });
118
+ };
119
+
120
+ /*----------------------------------------*/
121
+ /* Fatal Error */
122
+ /*----------------------------------------*/
123
+
124
+ // Stored copies of setters
125
+ let setFatalErrorMessage: (message: string) => void;
126
+ let setFatalErrorCode: (code: string) => void;
127
+ let setFatalErrorTitle: (title: string) => void;
128
+
129
+ /**
130
+ * Show a fatal error message
131
+ * @author Gabe Abrams
132
+ * @param error the error to show
133
+ * @param [errorTitle] title of the error box
134
+ */
135
+ export const showFatalError = (
136
+ error: any,
137
+ errorTitle: string = 'An Error Occurred',
138
+ ): undefined => {
139
+ // Determine message and code
140
+ const message: string = (
141
+ typeof error === 'string'
142
+ ? error.trim()
143
+ : String(error.message ?? 'An unknown error occurred.')
144
+ );
145
+ const code: string = (
146
+ typeof error === 'string'
147
+ ? ReactKitErrorCode.NoCode
148
+ : String(error.code ?? ReactKitErrorCode.NoCode)
149
+ );
150
+
151
+ // Handle case where app hasn't loaded
152
+ if (!setFatalErrorMessage || !setFatalErrorCode) {
153
+ alert(
154
+ errorTitle,
155
+ `${message} (code: ${code}). Please contact support.`,
156
+ );
157
+ return undefined;
158
+ }
159
+
160
+ // Use setters
161
+ setFatalErrorMessage(message);
162
+ setFatalErrorCode(code);
163
+ setFatalErrorTitle(errorTitle);
164
+
165
+ return undefined;
166
+ };
167
+
168
+ /*----------------------------------------*/
169
+ /* Session Expired */
170
+ /*----------------------------------------*/
171
+
172
+ // Stored copies of setters
173
+ let setSessionHasExpired: (isExpired: boolean) => void;
174
+
175
+ /**
176
+ * Show the "session expired" message
177
+ * @author Gabe Abrams
178
+ */
179
+ export const showSessionExpiredMessage = (): undefined => {
180
+ setSessionHasExpired(true);
181
+ return undefined;
182
+ };
183
+
184
+ /*------------------------------------------------------------------------*/
185
+ /* Component */
186
+ /*------------------------------------------------------------------------*/
187
+
188
+ const AppWrapper: React.FC<Props> = (props: Props): React.ReactElement => {
189
+ /*------------------------------------------------------------------------*/
190
+ /* Setup */
191
+ /*------------------------------------------------------------------------*/
192
+
193
+ /* -------------- Props ------------- */
194
+
195
+ const {
196
+ children,
197
+ dark,
198
+ sessionExpiredMessage = 'Your session has expired. Please go back to Canvas and start over.',
199
+ } = props;
200
+
201
+ /* -------------- State ------------- */
202
+
203
+ // Fatal error
204
+ const [
205
+ fatalErrorMessage,
206
+ setFatalErrorMessageInner,
207
+ ] = useState<string | undefined>();
208
+ setFatalErrorMessage = setFatalErrorMessageInner;
209
+ const [
210
+ fatalErrorCode,
211
+ setFatalErrorCodeInner,
212
+ ] = useState<string | undefined>();
213
+ setFatalErrorCode = setFatalErrorCodeInner;
214
+ const [
215
+ fatalErrorTitle,
216
+ setFatalErrorTitleInner,
217
+ ] = useState<string | undefined>();
218
+ setFatalErrorTitle = setFatalErrorTitleInner;
219
+
220
+ // Alert
221
+ const [
222
+ alertInfo,
223
+ setAlertInfoInner,
224
+ ] = useState<{
225
+ title: string,
226
+ text: string
227
+ }>();
228
+ setAlertInfo = setAlertInfoInner;
229
+
230
+ // Confirm
231
+ const [
232
+ confirmInfo,
233
+ setConfirmInfoInner,
234
+ ] = useState<{
235
+ title: string,
236
+ text: string
237
+ }>();
238
+ setConfirmInfo = setConfirmInfoInner;
239
+
240
+ // Session expired
241
+ const [
242
+ sessionHasExpired,
243
+ setSessionHasExpiredInner,
244
+ ] = useState<boolean>(false);
245
+ setSessionHasExpired = setSessionHasExpiredInner;
246
+
247
+ /*------------------------------------------------------------------------*/
248
+ /* Render */
249
+ /*------------------------------------------------------------------------*/
250
+
251
+ /*----------------------------------------*/
252
+ /* Modal */
253
+ /*----------------------------------------*/
254
+
255
+ // Modal that may be defined
256
+ let modal: React.ReactNode;
257
+
258
+ /* -------------- Alert ------------- */
259
+
260
+ if (alertInfo) {
261
+ modal = (
262
+ <Modal
263
+ title={alertInfo.title}
264
+ type={ModalType.Okay}
265
+ onClose={() => {
266
+ // Alert closed
267
+ if (onAlertClosed) {
268
+ onAlertClosed();
269
+ }
270
+ }}
271
+ onTopOfOtherModals
272
+ >
273
+ {alertInfo.text}
274
+ </Modal>
275
+ );
276
+ }
277
+
278
+ /* ------------- Confirm ------------ */
279
+
280
+ if (confirmInfo) {
281
+ modal = (
282
+ <Modal
283
+ title={confirmInfo.title}
284
+ type={ModalType.OkayCancel}
285
+ onClose={(buttonType) => {
286
+ if (onConfirmClosed) {
287
+ onConfirmClosed(buttonType === ModalButtonType.Okay);
288
+ }
289
+ }}
290
+ dontAllowBackdropExit
291
+ >
292
+
293
+ </Modal>
294
+ );
295
+ }
296
+
297
+ /*----------------------------------------*/
298
+ /* Views */
299
+ /*----------------------------------------*/
300
+
301
+ // Body that will be filled with the current view
302
+ let body: React.ReactNode;
303
+
304
+ /* ----------- Fatal Error ---------- */
305
+
306
+ if (fatalErrorMessage || fatalErrorCode || sessionHasExpired) {
307
+ // Re-encapsulate in an error
308
+ const error = (
309
+ sessionHasExpired
310
+ ? new ErrorWithCode(
311
+ (fatalErrorMessage ?? 'An unknown error has occurred. Please contact support.'),
312
+ (fatalErrorCode ?? ReactKitErrorCode.NoCode),
313
+ )
314
+ : new ErrorWithCode(
315
+ sessionExpiredMessage,
316
+ ReactKitErrorCode.SessionExpired,
317
+ )
318
+ );
319
+
320
+ // Build error screen
321
+ body = (
322
+ <div
323
+ style={{
324
+ display: 'block',
325
+ width: '100vw',
326
+ minHeight: '100vh',
327
+ paddingTop: '2rem',
328
+ backgroundColor: (
329
+ dark
330
+ ? '#222'
331
+ : '#fff'
332
+ ),
333
+ }}
334
+ >
335
+ <ErrorBox
336
+ title={(
337
+ sessionHasExpired
338
+ ? 'Session Expired'
339
+ : fatalErrorTitle
340
+ )}
341
+ error={error}
342
+ />
343
+ </div>
344
+ );
345
+ }
346
+
347
+ /* --------------- App -------------- */
348
+
349
+ if (!body) {
350
+ body = (
351
+ <>
352
+ {children}
353
+ </>
354
+ );
355
+ }
356
+
357
+ /*----------------------------------------*/
358
+ /* Main UI */
359
+ /*----------------------------------------*/
360
+
361
+ return (
362
+ <>
363
+ {modal}
364
+ {body}
365
+ </>
366
+ );
367
+ };
368
+
369
+ /*------------------------------------------------------------------------*/
370
+ /* Wrap Up */
371
+ /*------------------------------------------------------------------------*/
372
+
373
+ // Export component
374
+ 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;