feat: add jump nav (#43)

This commit is contained in:
Adam Butterworth
2019-05-15 13:08:53 -06:00
committed by GitHub
parent 4ba3414bd8
commit a93dc4e3b1
5 changed files with 250 additions and 174 deletions

55
package-lock.json generated
View File

@@ -1009,9 +1009,9 @@
"integrity": "sha512-APBpZvdQrC1MJWMzk33V7FR2RhBRtnH2QPLqZzS+qia7PixwgWNlnX7UfHjhx+YWkM53GdsZKs40EBkSwADuMA=="
},
"@edx/edx-bootstrap": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@edx/edx-bootstrap/-/edx-bootstrap-2.1.0.tgz",
"integrity": "sha512-9/CkIP1IsldKvOAS/T2OYeBnyuNWwX+u5GVR1kxw1YUCQbFw0W2M/Nz0HbIhG82H4O64uQqfdxnd1QpRJn07LA==",
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/@edx/edx-bootstrap/-/edx-bootstrap-2.2.1.tgz",
"integrity": "sha512-HkQ45u46ejX7WYwJX6ar/0FGBjx4F7bZvt3Dd4A677st0ic/47X5vQlqMqmK/M08C+GXaunNbZdbyF/ROrmwxw==",
"requires": {
"bootstrap": "^4.3.1"
}
@@ -8638,8 +8638,7 @@
"ansi-regex": {
"version": "2.1.1",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"aproba": {
"version": "1.2.0",
@@ -8660,14 +8659,12 @@
"balanced-match": {
"version": "1.0.0",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"brace-expansion": {
"version": "1.1.11",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@@ -8682,20 +8679,17 @@
"code-point-at": {
"version": "1.1.0",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"concat-map": {
"version": "0.0.1",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"console-control-strings": {
"version": "1.1.0",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"core-util-is": {
"version": "1.0.2",
@@ -8812,8 +8806,7 @@
"inherits": {
"version": "2.0.3",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"ini": {
"version": "1.3.5",
@@ -8825,7 +8818,6 @@
"version": "1.0.0",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"number-is-nan": "^1.0.0"
}
@@ -8840,7 +8832,6 @@
"version": "3.0.4",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"brace-expansion": "^1.1.7"
}
@@ -8848,14 +8839,12 @@
"minimist": {
"version": "0.0.8",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"minipass": {
"version": "2.3.5",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"safe-buffer": "^5.1.2",
"yallist": "^3.0.0"
@@ -8874,7 +8863,6 @@
"version": "0.5.1",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"minimist": "0.0.8"
}
@@ -8955,8 +8943,7 @@
"number-is-nan": {
"version": "1.0.1",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"object-assign": {
"version": "4.1.1",
@@ -8968,7 +8955,6 @@
"version": "1.4.0",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"wrappy": "1"
}
@@ -9054,8 +9040,7 @@
"safe-buffer": {
"version": "5.1.2",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"safer-buffer": {
"version": "2.1.2",
@@ -9091,7 +9076,6 @@
"version": "1.0.2",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0",
@@ -9111,7 +9095,6 @@
"version": "3.0.1",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"ansi-regex": "^2.0.0"
}
@@ -9155,14 +9138,12 @@
"wrappy": {
"version": "1.0.2",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"yallist": {
"version": "3.0.3",
"bundled": true,
"dev": true,
"optional": true
"dev": true
}
}
},
@@ -15759,6 +15740,14 @@
"warning": "^4.0.1"
}
},
"react-router-hash-link": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/react-router-hash-link/-/react-router-hash-link-1.2.1.tgz",
"integrity": "sha512-ddkCtmk/JwMmuU087TGShQHYyNjsJ+/9CTyuVdvvKf6ACgqk2Ma9ndX2xogo7WWmyq9AjuziBm5bmJ12zBxtsQ==",
"requires": {
"prop-types": "^15.6.0"
}
},
"react-test-renderer": {
"version": "16.8.6",
"resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-16.8.6.tgz",

View File

@@ -25,7 +25,7 @@
},
"dependencies": {
"@cospired/i18n-iso-languages": "^2.0.2",
"@edx/edx-bootstrap": "^2.1.0",
"@edx/edx-bootstrap": "^2.2.1",
"@edx/frontend-analytics": "^1.0.0",
"@edx/frontend-auth": "^5.3.0",
"@edx/frontend-component-footer": "^4.1.2",
@@ -62,6 +62,7 @@
"react-redux": "^5.1.1",
"react-router": "^4.2.0",
"react-router-dom": "^4.2.2",
"react-router-hash-link": "^1.2.1",
"react-transition-group": "^2.5.3",
"redux": "^4.0.1",
"redux-devtools-extension": "^2.13.2",

View File

@@ -16,6 +16,7 @@ import { fetchSettings, saveSettings, updateDraft } from './actions';
import { accountSettingsPageSelector } from './selectors';
import { PageLoading } from '../common';
import JumpNav from './components/JumpNav';
import Alert from './components/Alert';
import EditableField from './components/EditableField';
import PasswordReset from './components/PasswordReset';
@@ -152,150 +153,158 @@ class AccountSettingsPage extends React.Component {
);
return (
<div>
<div className="row">
<div className="col-md-8 col-lg-6">
<React.Fragment>
<section id="basic-information">
<h2 className="section-heading">
{this.props.intl.formatMessage(messages['account.settings.section.account.information'])}
</h2>
<p>{this.props.intl.formatMessage(messages['account.settings.section.account.information.description'])}</p>
{this.renderManagedProfileMessage()}
<h2 className="h4">{this.props.intl.formatMessage(messages['account.settings.section.account.information'])}</h2>
<p>{this.props.intl.formatMessage(messages['account.settings.section.account.information.description'])}</p>
{this.renderManagedProfileMessage()}
<EditableField
name="username"
type="text"
value={this.props.formValues.username}
label={this.props.intl.formatMessage(messages['account.settings.field.username'])}
helpText={this.props.intl.formatMessage(messages['account.settings.field.username.help.text'])}
isEditable={false}
{...editableFieldProps}
/>
<EditableField
name="name"
type="text"
value={this.props.formValues.name}
label={this.props.intl.formatMessage(messages['account.settings.field.full.name'])}
helpText={this.props.intl.formatMessage(messages['account.settings.field.full.name.help.text'])}
isEditable={!this.props.staticFields.includes('name')}
{...editableFieldProps}
/>
<EmailField
name="email"
label={this.props.intl.formatMessage(messages['account.settings.field.email'])}
value={this.props.formValues.email}
confirmationMessageDefinition={messages['account.settings.field.email.confirmation']}
helpText={this.props.intl.formatMessage(messages['account.settings.field.email.help.text'])}
isEditable={!this.props.staticFields.includes('email')}
{...editableFieldProps}
/>
{this.renderSecondaryEmailField(editableFieldProps)}
<PasswordReset />
<EditableField
name="year_of_birth"
type="select"
label={this.props.intl.formatMessage(messages['account.settings.field.dob'])}
value={this.props.formValues.year_of_birth}
options={YEAR_OF_BIRTH_OPTIONS}
{...editableFieldProps}
/>
<EditableField
name="country"
type="select"
value={this.props.formValues.country}
options={this.props.countryOptions}
label={this.props.intl.formatMessage(messages['account.settings.field.country'])}
isEditable={!this.props.staticFields.includes('country')}
{...editableFieldProps}
/>
</section>
<EditableField
name="username"
type="text"
value={this.props.formValues.username}
label={this.props.intl.formatMessage(messages['account.settings.field.username'])}
helpText={this.props.intl.formatMessage(messages['account.settings.field.username.help.text'])}
isEditable={false}
{...editableFieldProps}
/>
<EditableField
name="name"
type="text"
value={this.props.formValues.name}
label={this.props.intl.formatMessage(messages['account.settings.field.full.name'])}
helpText={this.props.intl.formatMessage(messages['account.settings.field.full.name.help.text'])}
isEditable={!this.props.staticFields.includes('name')}
{...editableFieldProps}
/>
<EmailField
name="email"
label={this.props.intl.formatMessage(messages['account.settings.field.email'])}
value={this.props.formValues.email}
confirmationMessageDefinition={messages['account.settings.field.email.confirmation']}
helpText={this.props.intl.formatMessage(messages['account.settings.field.email.help.text'])}
isEditable={!this.props.staticFields.includes('email')}
{...editableFieldProps}
/>
{this.renderSecondaryEmailField(editableFieldProps)}
<PasswordReset />
<EditableField
name="year_of_birth"
type="select"
label={this.props.intl.formatMessage(messages['account.settings.field.dob'])}
value={this.props.formValues.year_of_birth}
options={YEAR_OF_BIRTH_OPTIONS}
{...editableFieldProps}
/>
<EditableField
name="country"
type="select"
value={this.props.formValues.country}
options={this.props.countryOptions}
label={this.props.intl.formatMessage(messages['account.settings.field.country'])}
isEditable={!this.props.staticFields.includes('country')}
{...editableFieldProps}
/>
<section id="profile-information">
<h2 className="section-heading">
{this.props.intl.formatMessage(messages['account.settings.section.profile.information'])}
</h2>
<EditableField
name="level_of_education"
type="select"
value={this.props.formValues.level_of_education}
options={this.educationLevels}
label={this.props.intl.formatMessage(messages['account.settings.field.education'])}
{...editableFieldProps}
/>
<EditableField
name="gender"
type="select"
value={this.props.formValues.gender}
options={this.genderOptions}
label={this.props.intl.formatMessage(messages['account.settings.field.gender'])}
{...editableFieldProps}
/>
<EditableField
name="language_proficiencies"
type="select"
value={this.props.formValues.language_proficiencies}
options={this.props.languageProficiencyOptions}
label={this.props.intl.formatMessage(messages['account.settings.field.language.proficiencies'])}
{...editableFieldProps}
/>
</section>
<h2 className="h4">{this.props.intl.formatMessage(messages['account.settings.section.profile.information'])}</h2>
<section id="social-media">
<h2 className="section-heading">
{this.props.intl.formatMessage(messages['account.settings.section.social.media'])}
</h2>
<p>{this.props.intl.formatMessage(messages['account.settings.section.social.media.description'])}</p>
<EditableField
name="level_of_education"
type="select"
value={this.props.formValues.level_of_education}
options={this.educationLevels}
label={this.props.intl.formatMessage(messages['account.settings.field.education'])}
{...editableFieldProps}
/>
<EditableField
name="gender"
type="select"
value={this.props.formValues.gender}
options={this.genderOptions}
label={this.props.intl.formatMessage(messages['account.settings.field.gender'])}
{...editableFieldProps}
/>
<EditableField
name="language_proficiencies"
type="select"
value={this.props.formValues.language_proficiencies}
options={this.props.languageProficiencyOptions}
label={this.props.intl.formatMessage(messages['account.settings.field.language.proficiencies'])}
{...editableFieldProps}
/>
<EditableField
name="social_link_linkedin"
type="text"
value={this.props.formValues.social_link_linkedin}
label={this.props.intl.formatMessage(messages['account.settings.field.social.platform.name.linkedin'])}
{...editableFieldProps}
/>
<EditableField
name="social_link_facebook"
type="text"
value={this.props.formValues.social_link_facebook}
label={this.props.intl.formatMessage(messages['account.settings.field.social.platform.name.facebook'])}
{...editableFieldProps}
/>
<EditableField
name="social_link_twitter"
type="text"
value={this.props.formValues.social_link_twitter}
label={this.props.intl.formatMessage(messages['account.settings.field.social.platform.name.twitter'])}
{...editableFieldProps}
/>
</section>
<section id="site-preferences">
<h2 className="section-heading">
{this.props.intl.formatMessage(messages['account.settings.section.site.preferences'])}
</h2>
<h2 className="h4">{this.props.intl.formatMessage(messages['account.settings.section.social.media'])}</h2>
<p>{this.props.intl.formatMessage(messages['account.settings.section.social.media.description'])}</p>
<BetaLanguageBanner />
<EditableField
name="siteLanguage"
type="select"
options={this.props.siteLanguageOptions}
value={this.props.siteLanguage.draftOrSavedValue}
label={this.props.intl.formatMessage(messages['account.settings.field.site.language'])}
helpText={this.props.intl.formatMessage(messages['account.settings.field.site.language.help.text'])}
{...editableFieldProps}
/>
<EditableField
name="time_zone"
type="select"
value={this.props.formValues.time_zone || ''}
options={timeZoneOptions}
label={this.props.intl.formatMessage(messages['account.settings.field.time.zone'])}
helpText={this.props.intl.formatMessage(messages['account.settings.field.time.zone.description'])}
{...editableFieldProps}
onSubmit={(formId, value) => {
// the endpoint will not accept an empty string. it must be null
this.handleSubmit(formId, value || null);
}}
/>
</section>
<EditableField
name="social_link_linkedin"
type="text"
value={this.props.formValues.social_link_linkedin}
label={this.props.intl.formatMessage(messages['account.settings.field.social.platform.name.linkedin'])}
{...editableFieldProps}
/>
<EditableField
name="social_link_facebook"
type="text"
value={this.props.formValues.social_link_facebook}
label={this.props.intl.formatMessage(messages['account.settings.field.social.platform.name.facebook'])}
{...editableFieldProps}
/>
<EditableField
name="social_link_twitter"
type="text"
value={this.props.formValues.social_link_twitter}
label={this.props.intl.formatMessage(messages['account.settings.field.social.platform.name.twitter'])}
{...editableFieldProps}
/>
<h2 className="h4">{this.props.intl.formatMessage(messages['account.settings.section.site.preferences'])}</h2>
<BetaLanguageBanner />
<EditableField
name="siteLanguage"
type="select"
options={this.props.siteLanguageOptions}
value={this.props.siteLanguage.draftOrSavedValue}
label={this.props.intl.formatMessage(messages['account.settings.field.site.language'])}
helpText={this.props.intl.formatMessage(messages['account.settings.field.site.language.help.text'])}
{...editableFieldProps}
/>
<EditableField
name="time_zone"
type="select"
value={this.props.formValues.time_zone || ''}
options={timeZoneOptions}
label={this.props.intl.formatMessage(messages['account.settings.field.time.zone'])}
helpText={this.props.intl.formatMessage(messages['account.settings.field.time.zone.description'])}
{...editableFieldProps}
onSubmit={(formId, value) => {
// the endpoint will not accept an empty string. it must be null
this.handleSubmit(formId, value || null);
}}
/>
<h2 className="h4">{this.props.intl.formatMessage(messages['account.settings.section.linked.accounts'])}</h2>
<p>{this.props.intl.formatMessage(messages['account.settings.section.linked.accounts.description'])}</p>
<ThirdPartyAuth />
</div>
</div>
</div>
<section id="linked-accounts">
<h2 className="section-heading">{this.props.intl.formatMessage(messages['account.settings.section.linked.accounts'])}</h2>
<p>{this.props.intl.formatMessage(messages['account.settings.section.linked.accounts.description'])}</p>
<ThirdPartyAuth />
</section>
</React.Fragment>
);
}
@@ -325,12 +334,21 @@ class AccountSettingsPage extends React.Component {
return (
<div className="page__account-settings container-fluid py-5">
{this.renderDuplicateTpaProviderMessage()}
<h1>
<h1 className="mb-4">
{this.props.intl.formatMessage(messages['account.settings.page.heading'])}
</h1>
{loading ? this.renderLoading() : null}
{loaded ? this.renderContent() : null}
{loadingError ? this.renderError() : null}
<div>
<div className="row">
<div className="col-md-3">
<JumpNav />
</div>
<div className="col-md-9">
{loading ? this.renderLoading() : null}
{loaded ? this.renderContent() : null}
{loadingError ? this.renderError() : null}
</div>
</div>
</div>
</div>
);
}

View File

@@ -11,4 +11,24 @@
padding: 0;
display: inline-block;
}
.jump-nav {
@media (min-width: map-get($grid-breakpoints, "sm")) {
padding-top: 1rem;
position: sticky;
top: 1rem;
}
li {
margin-bottom: .5rem;
}
}
.section-heading {
@extend .h4;
margin-bottom: map-get($spacers, 3);
}
section {
// These properties together will shift the hashlink position
padding-top: 1rem;
}
}

View File

@@ -0,0 +1,48 @@
import React from 'react';
import { injectIntl, intlShape } from '@edx/frontend-i18n'; // eslint-disable-line
import { NavHashLink } from 'react-router-hash-link';
import messages from '../AccountSettingsPage.messages';
function JumpNav({ intl }) {
return (
<div className="jump-nav">
<ul className="list-unstyled">
<li>
<NavHashLink to="#basic-information">
{intl.formatMessage(messages['account.settings.section.account.information'])}
</NavHashLink>
</li>
<li>
<NavHashLink to="#profile-information">
{intl.formatMessage(messages['account.settings.section.profile.information'])}
</NavHashLink>
</li>
<li>
<NavHashLink to="#social-media">
{intl.formatMessage(messages['account.settings.section.social.media'])}
</NavHashLink>
</li>
<li>
<NavHashLink to="#site-preferences">
{intl.formatMessage(messages['account.settings.section.site.preferences'])}
</NavHashLink>
</li>
<li>
<NavHashLink to="#linked-accounts">
{intl.formatMessage(messages['account.settings.section.linked.accounts'])}
</NavHashLink>
</li>
</ul>
</div>
);
}
JumpNav.propTypes = {
intl: intlShape.isRequired,
};
export default injectIntl(JumpNav);