Compare commits

...

1 Commits

Author SHA1 Message Date
Adam Butterworth
ad85e2cebc Upgrade frontend build and satisfy new linting rules 2020-03-23 17:25:45 -04:00
16 changed files with 3187 additions and 3850 deletions

6849
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -12,7 +12,7 @@
"build": "fedx-scripts webpack", "build": "fedx-scripts webpack",
"i18n_extract": "BABEL_ENV=i18n fedx-scripts babel src --quiet > /dev/null", "i18n_extract": "BABEL_ENV=i18n fedx-scripts babel src --quiet > /dev/null",
"is-es5": "es-check es5 ./dist/*.js", "is-es5": "es-check es5 ./dist/*.js",
"lint": "fedx-scripts eslint", "lint": "fedx-scripts eslint . --ext .js,.jsx",
"snapshot": "fedx-scripts jest --updateSnapshot", "snapshot": "fedx-scripts jest --updateSnapshot",
"start": "fedx-scripts webpack-dev-server --progress", "start": "fedx-scripts webpack-dev-server --progress",
"test": "fedx-scripts jest --coverage --passWithNoTests" "test": "fedx-scripts jest --coverage --passWithNoTests"
@@ -73,7 +73,7 @@
"universal-cookie": "4.0.3" "universal-cookie": "4.0.3"
}, },
"devDependencies": { "devDependencies": {
"@edx/frontend-build": "2.0.6", "@edx/frontend-build": "3.1.14",
"codecov": "3.6.5", "codecov": "3.6.5",
"enzyme": "3.10.0", "enzyme": "3.10.0",
"enzyme-adapter-react-16": "1.15.2", "enzyme-adapter-react-16": "1.15.2",

View File

@@ -105,16 +105,6 @@ class AccountSettingsPage extends React.Component {
})), })),
})); }));
isEditable(fieldName) {
return !this.props.staticFields.includes(fieldName);
}
isManagedProfile() {
// Enterprise customer profiles are managed by their organizations. We determine whether
// a profile is managed or not by the presence of the profileDataManager prop.
return Boolean(this.props.profileDataManager);
}
handleEditableFieldChange = (name, value) => { handleEditableFieldChange = (name, value) => {
this.props.updateDraft(name, value); this.props.updateDraft(name, value);
}; };
@@ -123,6 +113,17 @@ class AccountSettingsPage extends React.Component {
this.props.saveSettings(formId, values); this.props.saveSettings(formId, values);
}; };
isManagedProfile() {
// Enterprise customer profiles are managed by their organizations. We determine whether
// a profile is managed or not by the presence of the profileDataManager prop.
return Boolean(this.props.profileDataManager);
}
isEditable(fieldName) {
return !this.props.staticFields.includes(fieldName);
}
renderDuplicateTpaProviderMessage() { renderDuplicateTpaProviderMessage() {
if (!this.state.duplicateTpaProvider) { if (!this.state.duplicateTpaProvider) {
return null; return null;
@@ -224,7 +225,7 @@ class AccountSettingsPage extends React.Component {
const hasLinkedTPA = findIndex(this.props.tpaProviders, provider => provider.connected) >= 0; const hasLinkedTPA = findIndex(this.props.tpaProviders, provider => provider.connected) >= 0;
return ( return (
<React.Fragment> <>
<div className="account-section" id="basic-information"> <div className="account-section" id="basic-information">
<h2 className="section-heading"> <h2 className="section-heading">
{this.props.intl.formatMessage(messages['account.settings.section.account.information'])} {this.props.intl.formatMessage(messages['account.settings.section.account.information'])}
@@ -247,9 +248,9 @@ class AccountSettingsPage extends React.Component {
value={this.props.formValues.name} value={this.props.formValues.name}
label={this.props.intl.formatMessage(messages['account.settings.field.full.name'])} label={this.props.intl.formatMessage(messages['account.settings.field.full.name'])}
emptyLabel={ emptyLabel={
this.isEditable('name') ? this.isEditable('name')
this.props.intl.formatMessage(messages['account.settings.field.full.name.empty']) : ? this.props.intl.formatMessage(messages['account.settings.field.full.name.empty'])
this.renderEmptyStaticFieldMessage() : this.renderEmptyStaticFieldMessage()
} }
helpText={this.props.intl.formatMessage(messages['account.settings.field.full.name.help.text'])} helpText={this.props.intl.formatMessage(messages['account.settings.field.full.name.help.text'])}
isEditable={this.isEditable('name')} isEditable={this.isEditable('name')}
@@ -259,9 +260,9 @@ class AccountSettingsPage extends React.Component {
name="email" name="email"
label={this.props.intl.formatMessage(messages['account.settings.field.email'])} label={this.props.intl.formatMessage(messages['account.settings.field.email'])}
emptyLabel={ emptyLabel={
this.isEditable('email') ? this.isEditable('email')
this.props.intl.formatMessage(messages['account.settings.field.email.empty']) : ? this.props.intl.formatMessage(messages['account.settings.field.email.empty'])
this.renderEmptyStaticFieldMessage() : this.renderEmptyStaticFieldMessage()
} }
value={this.props.formValues.email} value={this.props.formValues.email}
confirmationMessageDefinition={messages['account.settings.field.email.confirmation']} confirmationMessageDefinition={messages['account.settings.field.email.confirmation']}
@@ -287,9 +288,9 @@ class AccountSettingsPage extends React.Component {
options={countryOptions} options={countryOptions}
label={this.props.intl.formatMessage(messages['account.settings.field.country'])} label={this.props.intl.formatMessage(messages['account.settings.field.country'])}
emptyLabel={ emptyLabel={
this.isEditable('country') ? this.isEditable('country')
this.props.intl.formatMessage(messages['account.settings.field.country.empty']) : ? this.props.intl.formatMessage(messages['account.settings.field.country.empty'])
this.renderEmptyStaticFieldMessage() : this.renderEmptyStaticFieldMessage()
} }
isEditable={this.isEditable('country')} isEditable={this.isEditable('country')}
{...editableFieldProps} {...editableFieldProps}
@@ -328,14 +329,15 @@ class AccountSettingsPage extends React.Component {
emptyLabel={this.props.intl.formatMessage(messages['account.settings.field.language.proficiencies.empty'])} emptyLabel={this.props.intl.formatMessage(messages['account.settings.field.language.proficiencies.empty'])}
{...editableFieldProps} {...editableFieldProps}
/> />
{getConfig().COACHING_ENABLED && {getConfig().COACHING_ENABLED
this.props.formValues.coaching.eligible_for_coaching && && this.props.formValues.coaching.eligible_for_coaching
&& (
<CoachingToggle <CoachingToggle
name="coaching" name="coaching"
phone_number={this.props.formValues.phone_number} phone_number={this.props.formValues.phone_number}
coaching={this.props.formValues.coaching} coaching={this.props.formValues.coaching}
/> />
} )}
</div> </div>
<div className="account-section" id="social-media"> <div className="account-section" id="social-media">
@@ -414,7 +416,7 @@ class AccountSettingsPage extends React.Component {
/> />
</div> </div>
</React.Fragment> </>
); );
} }

View File

@@ -2,7 +2,9 @@ import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Button, Input, StatefulButton, ValidationFormGroup } from '@edx/paragon'; import {
Button, Input, StatefulButton, ValidationFormGroup,
} from '@edx/paragon';
import { faPencilAlt } from '@fortawesome/free-solid-svg-icons'; import { faPencilAlt } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
@@ -65,20 +67,20 @@ function EditableField(props) {
}; };
const renderValue = (rawValue) => { const renderValue = (rawValue) => {
if (!rawValue) return renderEmptyLabel(); if (!rawValue) { return renderEmptyLabel(); }
if (options) { if (options) {
// Use == instead of === to prevent issues when HTML casts numbers as strings // Use == instead of === to prevent issues when HTML casts numbers as strings
// eslint-disable-next-line eqeqeq // eslint-disable-next-line eqeqeq
const selectedOption = options.find(option => option.value == rawValue); const selectedOption = options.find(option => option.value == rawValue);
if (selectedOption) return selectedOption.label; if (selectedOption) { return selectedOption.label; }
} }
return rawValue; return rawValue;
}; };
const renderConfirmationMessage = () => { const renderConfirmationMessage = () => {
if (!confirmationMessageDefinition || !confirmationValue) return null; if (!confirmationMessageDefinition || !confirmationValue) { return null; }
return intl.formatMessage(confirmationMessageDefinition, { return intl.formatMessage(confirmationMessageDefinition, {
value: confirmationValue, value: confirmationValue,
}); });
@@ -123,7 +125,7 @@ function EditableField(props) {
// Swallowing the onSubmit event on the form would be better, but // Swallowing the onSubmit event on the form would be better, but
// we would have to add that logic for every field given our // we would have to add that logic for every field given our
// current structure of the application. // current structure of the application.
if (saveState === 'pending') e.preventDefault(); if (saveState === 'pending') { e.preventDefault(); }
}} }}
disabledStates={[]} disabledStates={[]}
/> />

View File

@@ -2,7 +2,9 @@ import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n'; import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n';
import { Button, StatefulButton, Input, ValidationFormGroup } from '@edx/paragon'; import {
Button, StatefulButton, Input, ValidationFormGroup,
} from '@edx/paragon';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faExclamationTriangle, faPencilAlt } from '@fortawesome/free-solid-svg-icons'; import { faExclamationTriangle, faPencilAlt } from '@fortawesome/free-solid-svg-icons';
@@ -56,7 +58,7 @@ function EmailField(props) {
}; };
const renderConfirmationMessage = () => { const renderConfirmationMessage = () => {
if (!confirmationMessageDefinition || !confirmationValue) return null; if (!confirmationMessageDefinition || !confirmationValue) { return null; }
return ( return (
<Alert <Alert
className="alert-warning mt-n2" className="alert-warning mt-n2"
@@ -91,7 +93,7 @@ function EmailField(props) {
}; };
const renderValue = () => { const renderValue = () => {
if (confirmationValue) return renderConfirmationValue(); if (confirmationValue) { return renderConfirmationValue(); }
return value || renderEmptyLabel(); return value || renderEmptyLabel();
}; };
@@ -132,7 +134,7 @@ function EmailField(props) {
// Swallowing the onSubmit event on the form would be better, but // Swallowing the onSubmit event on the form would be better, but
// we would have to add that logic for every field given our // we would have to add that logic for every field given our
// current structure of the application. // current structure of the application.
if (saveState === 'pending') e.preventDefault(); if (saveState === 'pending') { e.preventDefault(); }
}} }}
disabledStates={[]} disabledStates={[]}
/> />

View File

@@ -11,7 +11,7 @@ const onChildExit = (htmlNode) => {
const enteringChild = htmlNode.previousSibling || htmlNode.nextSibling; const enteringChild = htmlNode.previousSibling || htmlNode.nextSibling;
// There's no replacement, do nothing. // There's no replacement, do nothing.
if (!enteringChild) return; if (!enteringChild) { return; }
// Get all the focusable elements in the entering child and focus the first one // Get all the focusable elements in the entering child and focus the first one
const focusableElements = enteringChild.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'); const focusableElements = enteringChild.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
@@ -29,7 +29,7 @@ function SwitchContent({ expression, cases, className }) {
return getContent(cases[caseKey]); return getContent(cases[caseKey]);
} }
return React.cloneElement(cases[caseKey], { key: caseKey }); return React.cloneElement(cases[caseKey], { key: caseKey });
} else if (cases.default) { } if (cases.default) {
if (typeof cases.default === 'string') { if (typeof cases.default === 'string') {
return getContent(cases.default); return getContent(cases.default);
} }

View File

@@ -47,11 +47,12 @@ const reducer = (state = defaultState, action) => {
case FETCH_SETTINGS.SUCCESS: case FETCH_SETTINGS.SUCCESS:
return { return {
...state, ...state,
values: Object.assign({}, state.values, action.payload.values), values: { ...state.values, ...action.payload.values },
// Dump the providers into thirdPartyAuth. // Dump the providers into thirdPartyAuth.
thirdPartyAuth: Object.assign({}, state.thirdPartyAuth, { thirdPartyAuth: {
...state.thirdPartyAuth,
providers: action.payload.thirdPartyAuthProviders, providers: action.payload.thirdPartyAuthProviders,
}), },
profileDataManager: action.payload.profileDataManager, profileDataManager: action.payload.profileDataManager,
timeZones: action.payload.timeZones, timeZones: action.payload.timeZones,
loading: false, loading: false,
@@ -96,9 +97,10 @@ const reducer = (state = defaultState, action) => {
case UPDATE_DRAFT: case UPDATE_DRAFT:
return { return {
...state, ...state,
drafts: Object.assign({}, state.drafts, { drafts: {
...state.drafts,
[action.payload.name]: action.payload.value, [action.payload.name]: action.payload.value,
}), },
saveState: null, saveState: null,
errors: {}, errors: {},
}; };
@@ -119,19 +121,18 @@ const reducer = (state = defaultState, action) => {
return { return {
...state, ...state,
saveState: 'complete', saveState: 'complete',
values: Object.assign({}, state.values, action.payload.values), values: { ...state.values, ...action.payload.values },
errors: {}, errors: {},
confirmationValues: Object.assign( confirmationValues: {
{}, ...state.confirmationValues,
state.confirmationValues, ...action.payload.confirmationValues,
action.payload.confirmationValues, },
),
}; };
case SAVE_SETTINGS.FAILURE: case SAVE_SETTINGS.FAILURE:
return { return {
...state, ...state,
saveState: 'error', saveState: 'error',
errors: Object.assign({}, state.errors, action.payload.errors), errors: { ...state.errors, ...action.payload.errors },
}; };
case SAVE_SETTINGS.RESET: case SAVE_SETTINGS.RESET:
return { return {

View File

@@ -1,4 +1,6 @@
import { call, put, delay, takeEvery, all } from 'redux-saga/effects'; import {
call, put, delay, takeEvery, all,
} from 'redux-saga/effects';
import { publish } from '@edx/frontend-platform'; import { publish } from '@edx/frontend-platform';
import { getLocale, handleRtl, LOCALE_CHANGED } from '@edx/frontend-platform/i18n'; import { getLocale, handleRtl, LOCALE_CHANGED } from '@edx/frontend-platform/i18n';
@@ -48,7 +50,9 @@ export function* handleFetchSettings() {
userId, userId,
); );
if (values.country) yield put(fetchTimeZones(values.country)); if (values.country) {
yield put(fetchTimeZones(values.country));
}
yield put(fetchSettingsSuccess({ yield put(fetchSettingsSuccess({
values, values,
@@ -87,7 +91,9 @@ export function* handleSaveSettings(action) {
savedValues = yield call(patchSettings, username, commitData, userId); savedValues = yield call(patchSettings, username, commitData, userId);
} }
yield put(saveSettingsSuccess(savedValues, commitData)); yield put(saveSettingsSuccess(savedValues, commitData));
if (savedValues.country) yield put(fetchTimeZones(savedValues.country)); if (savedValues.country) {
yield put(fetchTimeZones(savedValues.country));
}
yield delay(1000); yield delay(1000);
yield put(closeForm(action.payload.formId)); yield put(closeForm(action.payload.formId));
} catch (e) { } catch (e) {

View File

@@ -41,7 +41,9 @@ function packAccountCommitData(commitData) {
SOCIAL_PLATFORMS.forEach(({ id, key }) => { SOCIAL_PLATFORMS.forEach(({ id, key }) => {
// Skip missing values. Empty strings are valid values and should be preserved. // Skip missing values. Empty strings are valid values and should be preserved.
if (commitData[key] === undefined) return; if (commitData[key] === undefined) {
return;
}
packedData.social_links = [{ platform: id, social_link: commitData[key] }]; packedData.social_links = [{ platform: id, social_link: commitData[key] }];
delete packedData[key]; delete packedData[key];

View File

@@ -4,9 +4,9 @@ import snakeCase from 'lodash.snakecase';
export function modifyObjectKeys(object, modify) { export function modifyObjectKeys(object, modify) {
// If the passed in object is not an object, return it. // If the passed in object is not an object, return it.
if ( if (
object === undefined || object === undefined
object === null || || object === null
(typeof object !== 'object' && !Array.isArray(object)) || (typeof object !== 'object' && !Array.isArray(object))
) { ) {
return object; return object;
} }

View File

@@ -1,7 +1,9 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Button, Input, Modal, ValidationFormGroup } from '@edx/paragon'; import {
Button, Input, Modal, ValidationFormGroup,
} from '@edx/paragon';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { faExclamationCircle, faExclamationTriangle } from '@fortawesome/free-solid-svg-icons'; import { faExclamationCircle, faExclamationTriangle } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
@@ -31,10 +33,9 @@ export class ConfirmationModal extends Component {
return null; return null;
} }
const headerMessageId = this.getShortErrorMessageId(errorType); const headerMessageId = this.getShortErrorMessageId(errorType);
const detailsMessageId = const detailsMessageId = reason === 'empty-password'
reason === 'empty-password' ? null
? null : 'account.settings.delete.account.error.unable.to.delete.details';
: 'account.settings.delete.account.error.unable.to.delete.details';
return ( return (
<Alert <Alert
@@ -66,7 +67,7 @@ export class ConfirmationModal extends Component {
<Modal <Modal
open={open} open={open}
title={intl.formatMessage(messages['account.settings.delete.account.modal.header'])} title={intl.formatMessage(messages['account.settings.delete.account.modal.header'])}
body={ body={(
<div> <div>
{this.renderError()} {this.renderError()}
<Alert <Alert
@@ -98,7 +99,7 @@ export class ConfirmationModal extends Component {
/> />
</ValidationFormGroup> </ValidationFormGroup>
</div> </div>
} )}
buttons={[ buttons={[
<Button className="btn-danger" onClick={onSubmit}> <Button className="btn-danger" onClick={onSubmit}>
{intl.formatMessage(messages['account.settings.delete.account.modal.confirm.delete'])} {intl.formatMessage(messages['account.settings.delete.account.modal.confirm.delete'])}

View File

@@ -24,9 +24,12 @@ import ConnectedSuccessModal from './SuccessModal';
import BeforeProceedingBanner from './BeforeProceedingBanner'; import BeforeProceedingBanner from './BeforeProceedingBanner';
export class DeleteAccount extends React.Component { export class DeleteAccount extends React.Component {
state = { constructor(props) {
password: '', super(props);
}; this.state = {
password: '',
};
}
handleSubmit = () => { handleSubmit = () => {
if (this.state.password === '') { if (this.state.password === '') {

View File

@@ -11,13 +11,13 @@ export const SuccessModal = (props) => {
<Modal <Modal
open={status === 'deleted'} open={status === 'deleted'}
title={intl.formatMessage(messages['account.settings.delete.account.modal.after.header'])} title={intl.formatMessage(messages['account.settings.delete.account.modal.after.header'])}
body={ body={(
<div> <div>
<p className="h6"> <p className="h6">
{intl.formatMessage(messages['account.settings.delete.account.modal.after.text'])} {intl.formatMessage(messages['account.settings.delete.account.modal.after.text'])}
</p> </p>
</div> </div>
} )}
closeText={intl.formatMessage(messages['account.settings.delete.account.modal.after.button'])} closeText={intl.formatMessage(messages['account.settings.delete.account.modal.after.button'])}
renderHeaderCloseButton={false} renderHeaderCloseButton={false}
onClose={onClose} onClose={onClose}

View File

@@ -12,9 +12,8 @@ export const siteLanguageListSelector = createSelector(
export const siteLanguageOptionsSelector = createSelector( export const siteLanguageOptionsSelector = createSelector(
siteLanguageSelector, siteLanguageSelector,
siteLanguage => siteLanguage => siteLanguage.siteLanguageList.map(({ code, name }) => ({
siteLanguage.siteLanguageList.map(({ code, name }) => ({ value: code,
value: code, label: name,
label: name, })),
})),
); );

View File

@@ -11,14 +11,16 @@ class ThirdPartyAuth extends Component {
onClickDisconnect = (e) => { onClickDisconnect = (e) => {
e.preventDefault(); e.preventDefault();
const providerId = e.currentTarget.getAttribute('data-provider-id'); const providerId = e.currentTarget.getAttribute('data-provider-id');
if (this.props.disconnectionStatuses[providerId] === 'pending') return; if (this.props.disconnectionStatuses[providerId] === 'pending') {
return;
}
const disconnectUrl = e.currentTarget.getAttribute('data-disconnect-url'); const disconnectUrl = e.currentTarget.getAttribute('data-disconnect-url');
this.props.disconnectAuth(disconnectUrl, providerId); this.props.disconnectAuth(disconnectUrl, providerId);
} }
renderUnconnectedProvider(url, name) { renderUnconnectedProvider(url, name) {
return ( return (
<React.Fragment> <>
<h6 aria-level="3">{name}</h6> <h6 aria-level="3">{name}</h6>
<Hyperlink destination={url} className="btn btn-outline-primary"> <Hyperlink destination={url} className="btn btn-outline-primary">
<FormattedMessage <FormattedMessage
@@ -28,7 +30,7 @@ class ThirdPartyAuth extends Component {
values={{ name }} values={{ name }}
/> />
</Hyperlink> </Hyperlink>
</React.Fragment> </>
); );
} }
@@ -36,7 +38,7 @@ class ThirdPartyAuth extends Component {
const hasError = this.props.errors[id]; const hasError = this.props.errors[id];
return ( return (
<React.Fragment> <>
<h6 aria-level="3"> <h6 aria-level="3">
{name} {name}
<span className="small font-weight-normal text-muted ml-2"> <span className="small font-weight-normal text-muted ml-2">
@@ -75,7 +77,7 @@ class ThirdPartyAuth extends Component {
data-disconnect-url={url} data-disconnect-url={url}
data-provider-id={id} data-provider-id={id}
/> />
</React.Fragment> </>
); );
} }
@@ -85,9 +87,9 @@ class ThirdPartyAuth extends Component {
return ( return (
<div className="form-group" key={id}> <div className="form-group" key={id}>
{ {
connected ? connected
this.renderConnectedProvider(disconnectUrl, name, id) : ? this.renderConnectedProvider(disconnectUrl, name, id)
this.renderUnconnectedProvider(connectUrl, name) : this.renderUnconnectedProvider(connectUrl, name)
} }
</div> </div>
); );
@@ -104,7 +106,7 @@ class ThirdPartyAuth extends Component {
} }
render() { render() {
if (this.props.providers === undefined) return null; if (this.props.providers === undefined) { return null; }
if (this.props.providers.length === 0) { if (this.props.providers.length === 0) {
return this.renderNoProviders(); return this.renderNoProviders();

View File

@@ -1,7 +1,9 @@
import 'babel-polyfill'; import 'babel-polyfill';
import 'formdata-polyfill'; import 'formdata-polyfill';
import { AppProvider, ErrorPage } from '@edx/frontend-platform/react'; import { AppProvider, ErrorPage } from '@edx/frontend-platform/react';
import { subscribe, initialize, APP_INIT_ERROR, APP_READY, mergeConfig } from '@edx/frontend-platform'; import {
subscribe, initialize, APP_INIT_ERROR, APP_READY, mergeConfig,
} from '@edx/frontend-platform';
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import { Route, Switch } from 'react-router-dom'; import { Route, Switch } from 'react-router-dom';