Organize application according to semantic modules. (#153)

This commit is contained in:
David Joy
2019-04-16 11:46:20 -04:00
committed by GitHub
parent 14023dbf32
commit d009d5ce6c
85 changed files with 725 additions and 555 deletions

View File

@@ -1,67 +1,167 @@
import React, { Component } from 'react';
import { connect, Provider } from 'react-redux';
import PropTypes from 'prop-types';
import { IntlProvider } from 'react-intl';
import { IntlProvider, injectIntl, intlShape } from 'react-intl';
import { Route, Switch } from 'react-router-dom';
import { ConnectedRouter } from 'connected-react-router';
import { sendTrackEvent } from '@edx/frontend-analytics';
import SiteHeader from '@edx/frontend-component-site-header';
import SiteFooter from '@edx/frontend-component-footer';
import { fetchUserAccount, UserAccountApiService } from '@edx/frontend-auth';
import apiClient from '../config/apiClient';
import { getLocale, getMessages } from '@edx/frontend-i18n'; // eslint-disable-line
import SiteHeader from './common/SiteHeader';
import ConnectedProfilePage from './ProfilePage';
import FooterLogo from '../../assets/edx-footer.png';
import { PageLoading, fetchUserAccount } from '../common';
import { ConnectedProfilePage } from '../profile';
import FooterLogo from '../assets/edx-footer.png';
import HeaderLogo from '../assets/logo.svg';
import ErrorPage from './ErrorPage';
import NotFoundPage from './NotFoundPage';
import PageLoading from './common/PageLoading';
import messages from './App.messages';
function PageContent({
ready,
configuration,
username,
avatar,
intl,
}) {
if (!ready) {
return <PageLoading />;
}
const mainMenu = [
{
type: 'item',
href: `${process.env.MARKETING_SITE_BASE_URL}/course`,
content: intl.formatMessage(messages['siteheader.links.courses']),
},
{
type: 'item',
href: `${process.env.MARKETING_SITE_BASE_URL}/course?program=all`,
content: intl.formatMessage(messages['siteheader.links.programs']),
},
{
type: 'item',
href: `${process.env.MARKETING_SITE_BASE_URL}/schools-partners`,
content: intl.formatMessage(messages['siteheader.links.schools']),
},
];
const userMenu = [
{
type: 'item',
href: `${process.env.LMS_BASE_URL}`,
content: intl.formatMessage(messages['siteheader.user.menu.dashboard']),
},
{
type: 'item',
href: `${process.env.BASE_URL}/u/${username}`,
content: intl.formatMessage(messages['siteheader.user.menu.profile']),
},
{
type: 'item',
href: `${process.env.LMS_BASE_URL}/account/settings`,
content: intl.formatMessage(messages['siteheader.user.menu.account.settings']),
},
{
type: 'item',
href: process.env.LOGOUT_URL,
content: intl.formatMessage(messages['siteheader.user.menu.logout']),
},
];
const loggedOutItems = [
{
type: 'item',
href: `${process.env.LMS_BASE_URL}/login`,
content: intl.formatMessage(messages['siteheader.user.menu.login']),
},
{
type: 'item',
href: `${process.env.LMS_BASE_URL}/register`,
content: intl.formatMessage(messages['siteheader.user.menu.register']),
},
];
return (
<div>
<SiteHeader
logo={HeaderLogo}
loggedIn
username={username}
avatar={avatar}
logoAltText={configuration.SITE_NAME}
logoDestination={configuration.MARKETING_SITE_BASE_URL}
mainMenu={mainMenu}
userMenu={userMenu}
loggedOutItems={loggedOutItems}
/>
<main>
<Switch>
<Route path="/u/:username" component={ConnectedProfilePage} />
<Route path="/error" component={ErrorPage} />
<Route path="/notfound" component={NotFoundPage} />
<Route path="*" component={NotFoundPage} />
</Switch>
</main>
<SiteFooter
siteName={configuration.SITE_NAME}
siteLogo={FooterLogo}
marketingSiteBaseUrl={configuration.MARKETING_SITE_BASE_URL}
supportUrl={configuration.SUPPORT_URL}
contactUrl={configuration.CONTACT_URL}
openSourceUrl={configuration.OPEN_SOURCE_URL}
termsOfServiceUrl={configuration.TERMS_OF_SERVICE_URL}
privacyPolicyUrl={configuration.PRIVACY_POLICY_URL}
facebookUrl={configuration.FACEBOOK_URL}
twitterUrl={configuration.TWITTER_URL}
youTubeUrl={configuration.YOU_TUBE_URL}
linkedInUrl={configuration.LINKED_IN_URL}
googlePlusUrl={configuration.GOOGLE_PLUS_URL}
redditUrl={configuration.REDDIT_URL}
appleAppStoreUrl={configuration.APPLE_APP_STORE_URL}
googlePlayUrl={configuration.GOOGLE_PLAY_URL}
handleAllTrackEvents={sendTrackEvent}
/>
</div>
);
}
PageContent.propTypes = {
username: PropTypes.string.isRequired,
avatar: PropTypes.string,
ready: PropTypes.bool,
configuration: PropTypes.shape({
SITE_NAME: PropTypes.string.isRequired,
MARKETING_SITE_BASE_URL: PropTypes.string.isRequired,
SUPPORT_URL: PropTypes.string.isRequired,
CONTACT_URL: PropTypes.string.isRequired,
OPEN_SOURCE_URL: PropTypes.string.isRequired,
TERMS_OF_SERVICE_URL: PropTypes.string.isRequired,
PRIVACY_POLICY_URL: PropTypes.string.isRequired,
FACEBOOK_URL: PropTypes.string.isRequired,
TWITTER_URL: PropTypes.string.isRequired,
YOU_TUBE_URL: PropTypes.string.isRequired,
LINKED_IN_URL: PropTypes.string.isRequired,
GOOGLE_PLUS_URL: PropTypes.string.isRequired,
REDDIT_URL: PropTypes.string.isRequired,
APPLE_APP_STORE_URL: PropTypes.string.isRequired,
GOOGLE_PLAY_URL: PropTypes.string.isRequired,
}).isRequired,
intl: intlShape.isRequired,
};
PageContent.defaultProps = {
ready: false,
avatar: null,
};
const IntlPageContent = injectIntl(PageContent);
class App extends Component {
componentDidMount() {
const { username } = this.props;
const userAccountApiService = new UserAccountApiService(apiClient, process.env.LMS_BASE_URL);
this.props.fetchUserAccount(userAccountApiService, username);
}
renderContent() {
if (!this.props.ready) {
return <PageLoading />;
}
return (
<div>
<SiteHeader />
<main>
<Switch>
<Route path="/u/:username" component={ConnectedProfilePage} />
<Route path="/error" component={ErrorPage} />
<Route path="/notfound" component={NotFoundPage} />
<Route path="*" component={NotFoundPage} />
</Switch>
</main>
<SiteFooter
siteName={process.env.SITE_NAME}
siteLogo={FooterLogo}
marketingSiteBaseUrl={process.env.MARKETING_SITE_BASE_URL}
supportUrl={process.env.SUPPORT_URL}
contactUrl={process.env.CONTACT_URL}
openSourceUrl={process.env.OPEN_SOURCE_URL}
termsOfServiceUrl={process.env.TERMS_OF_SERVICE_URL}
privacyPolicyUrl={process.env.PRIVACY_POLICY_URL}
facebookUrl={process.env.FACEBOOK_URL}
twitterUrl={process.env.TWITTER_URL}
youTubeUrl={process.env.YOU_TUBE_URL}
linkedInUrl={process.env.LINKED_IN_URL}
googlePlusUrl={process.env.GOOGLE_PLUS_URL}
redditUrl={process.env.REDDIT_URL}
appleAppStoreUrl={process.env.APPLE_APP_STORE_URL}
googlePlayUrl={process.env.GOOGLE_PLAY_URL}
handleAllTrackEvents={sendTrackEvent}
/>
</div>
);
this.props.fetchUserAccount(username);
}
render() {
@@ -69,7 +169,12 @@ class App extends Component {
<IntlProvider locale={getLocale()} messages={getMessages()}>
<Provider store={this.props.store}>
<ConnectedRouter history={this.props.history}>
{this.renderContent()}
<IntlPageContent
ready={this.props.ready}
configuration={this.props.configuration}
username={this.props.username}
avatar={this.props.avatar}
/>
</ConnectedRouter>
</Provider>
</IntlProvider>
@@ -80,13 +185,32 @@ class App extends Component {
App.propTypes = {
fetchUserAccount: PropTypes.func.isRequired,
username: PropTypes.string.isRequired,
avatar: PropTypes.string,
store: PropTypes.object.isRequired, // eslint-disable-line
history: PropTypes.object.isRequired, // eslint-disable-line
ready: PropTypes.bool,
configuration: PropTypes.shape({
SITE_NAME: PropTypes.string.isRequired,
MARKETING_SITE_BASE_URL: PropTypes.string.isRequired,
SUPPORT_URL: PropTypes.string.isRequired,
CONTACT_URL: PropTypes.string.isRequired,
OPEN_SOURCE_URL: PropTypes.string.isRequired,
TERMS_OF_SERVICE_URL: PropTypes.string.isRequired,
PRIVACY_POLICY_URL: PropTypes.string.isRequired,
FACEBOOK_URL: PropTypes.string.isRequired,
TWITTER_URL: PropTypes.string.isRequired,
YOU_TUBE_URL: PropTypes.string.isRequired,
LINKED_IN_URL: PropTypes.string.isRequired,
GOOGLE_PLUS_URL: PropTypes.string.isRequired,
REDDIT_URL: PropTypes.string.isRequired,
APPLE_APP_STORE_URL: PropTypes.string.isRequired,
GOOGLE_PLAY_URL: PropTypes.string.isRequired,
}).isRequired,
};
App.defaultProps = {
ready: false,
avatar: null,
};
const mapStateToProps = state => ({
@@ -94,6 +218,10 @@ const mapStateToProps = state => ({
// An error means that we tried to load the user account and failed,
// which also means we're ready to display something.
ready: state.userAccount.loaded || state.userAccount.error != null,
configuration: state.configuration,
avatar: state.userAccount.profileImage.hasImage
? state.userAccount.profileImage.imageUrlMedium
: null,
});
export default connect(

View File

@@ -1,46 +1,51 @@
import React, { Component } from 'react';
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { Button } from '@edx/paragon';
import { FormattedMessage } from 'react-intl';
import { Link } from 'react-router-dom';
import apiClient from '../config/apiClient';
export default class ErrorPage extends Component {
componentDidMount() {}
render() {
const { username } = apiClient.getAuthenticationState().authentication;
return (
<div className="container-fluid py-5 justify-content-center align-items-start text-center">
<div className="row">
<div className="col">
<p className="my-0 py-5 text-muted">
<FormattedMessage
id="profile.error.message.text"
defaultMessage="An unexpected error occurred. Please click the button below to return to your profile and try again."
description="error message when an unexpected error occurs"
/>
</p>
</div>
</div>
<div className="row">
<div className="col">
<Link to={`/u/${username}`}>
<Button
buttonType="primary"
label={
<FormattedMessage
id="profile.error.button.text"
defaultMessage="Return to Your Profile"
description="text for button that navigates back to your profile page after an error has occured"
/>
}
/>
</Link>
</div>
function ErrorPage({ username }) {
return (
<div className="container-fluid py-5 justify-content-center align-items-start text-center">
<div className="row">
<div className="col">
<p className="my-0 py-5 text-muted">
<FormattedMessage
id="profile.error.message.text"
defaultMessage="An unexpected error occurred. Please click the button below to return to your profile and try again."
description="error message when an unexpected error occurs"
/>
</p>
</div>
</div>
);
}
<div className="row">
<div className="col">
<Link to={`/u/${username}`}>
<Button
buttonType="primary"
label={
<FormattedMessage
id="profile.error.button.text"
defaultMessage="Return to Your Profile"
description="text for button that navigates back to your profile page after an error has occured"
/>
}
/>
</Link>
</div>
</div>
</div>
);
}
ErrorPage.propTypes = {
username: PropTypes.string.isRequired,
};
export default connect(
state => ({
username: state.authentication.username,
}),
{},
)(ErrorPage);

View File

@@ -1,23 +1,16 @@
import React, { Component } from 'react';
import React from 'react';
import { FormattedMessage } from 'react-intl';
export default class NotFoundPage extends Component {
componentDidMount() {}
render() {
return (
<div className="container-fluid d-flex py-5 justify-content-center align-items-start text-center">
<p
className="my-0 py-5 text-muted"
style={{ maxWidth: '32em' }}
>
<FormattedMessage
id="profile.notfound.message"
defaultMessage="The page you're looking for is unavailable or there's an error in the URL. Please check the URL and try again."
description="error message when a page does not exist"
/>
</p>
</div>
);
}
export default function NotFoundPage() {
return (
<div className="container-fluid d-flex py-5 justify-content-center align-items-start text-center">
<p className="my-0 py-5 text-muted" style={{ maxWidth: '32em' }}>
<FormattedMessage
id="profile.notfound.message"
defaultMessage="The page you're looking for is unavailable or there's an error in the URL. Please check the URL and try again."
description="error message when a page does not exist"
/>
</p>
</div>
);
}

View File

@@ -1,356 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { StatusAlert, Hyperlink } from '@edx/paragon';
import { connect } from 'react-redux';
import { injectIntl, intlShape } from '@edx/frontend-i18n'; // eslint-disable-line
// Analytics
import { sendTrackingLogEvent } from '@edx/frontend-analytics';
// Actions
import {
fetchProfile,
saveProfile,
saveProfilePhoto,
deleteProfilePhoto,
openForm,
closeForm,
updateDraft,
} from '../actions/ProfileActions';
// Components
import ProfileAvatar from './ProfilePage/ProfileAvatar';
import Name from './ProfilePage/Name';
import Country from './ProfilePage/Country';
import PreferredLanguage from './ProfilePage/PreferredLanguage';
import Education from './ProfilePage/Education';
import SocialLinks from './ProfilePage/SocialLinks';
import Bio from './ProfilePage/Bio';
import Certificates from './ProfilePage/Certificates';
import AgeMessage from './ProfilePage/AgeMessage';
import DateJoined from './ProfilePage/DateJoined';
import PageLoading from './common/PageLoading';
import Banner from './common/Banner';
import { profilePageSelector } from '../selectors/ProfilePageSelector';
// Configuration
import { configuration } from '../config/environment';
// i18n
import messages from './ProfilePage.messages';
export class ProfilePage extends React.Component {
constructor(props) {
super(props);
this.handleSaveProfilePhoto = this.handleSaveProfilePhoto.bind(this);
this.handleDeleteProfilePhoto = this.handleDeleteProfilePhoto.bind(this);
this.handleClose = this.handleClose.bind(this);
this.handleOpen = this.handleOpen.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
this.handleChange = this.handleChange.bind(this);
}
componentDidMount() {
this.props.fetchProfile(this.props.match.params.username);
sendTrackingLogEvent('edx.profile.viewed', {
username: this.props.match.params.username,
});
}
handleSaveProfilePhoto(formData) {
this.props.saveProfilePhoto(this.props.username, formData);
}
handleDeleteProfilePhoto() {
this.props.deleteProfilePhoto(this.props.username);
}
handleClose(formId) {
this.props.closeForm(formId);
}
handleOpen(formId) {
this.props.openForm(formId);
}
handleSubmit(formId) {
this.props.saveProfile(formId);
}
handleChange(name, value) {
this.props.updateDraft(name, value);
}
// Inserted into the DOM in two places (for responsive layout)
renderViewMyRecordsButton() {
if (!this.props.isAuthenticatedUserProfile) {
return null;
}
return (
<Hyperlink className="btn btn-primary" href={configuration.VIEW_MY_RECORDS_URL} target="_blank">
{this.props.intl.formatMessage(messages['profile.viewMyRecords'])}
</Hyperlink>
);
}
// Inserted into the DOM in two places (for responsive layout)
renderHeadingLockup() {
const { username, dateJoined } = this.props;
return (
<React.Fragment>
<h1 className="h2 mb-0 font-weight-bold">{username}</h1>
<DateJoined date={dateJoined} />
<hr className="d-none d-md-block" />
</React.Fragment>
);
}
renderPhotoUploadErrorMessage() {
const { photoUploadError } = this.props;
if (photoUploadError === null) {
return null;
}
return (
<div className="row">
<div className="col-md-4 col-lg-3">
<StatusAlert alertType="danger" dialog={photoUploadError.userMessage} dismissible={false} open />
</div>
</div>
);
}
render() {
const {
profileImage,
name,
visibilityName,
country,
visibilityCountry,
levelOfEducation,
visibilityLevelOfEducation,
socialLinks,
draftSocialLinksByPlatform,
visibilitySocialLinks,
languageProficiencies,
visibilityLanguageProficiencies,
visibilityCourseCertificates,
bio,
visibilityBio,
requiresParentalConsent,
isAuthenticatedUserProfile,
isLoadingProfile,
} = this.props;
if (isLoadingProfile) return <PageLoading />;
const commonFormProps = {
openHandler: this.handleOpen,
closeHandler: this.handleClose,
submitHandler: this.handleSubmit,
changeHandler: this.handleChange,
};
const shouldShowAgeMessage = requiresParentalConsent && isAuthenticatedUserProfile;
return (
<div className="profile-page">
<Banner />
<div className="container-fluid">
<div className="row align-items-center pt-4 mb-4 pt-md-0 mb-md-0">
<div className="col-auto col-md-4 col-lg-3">
<div className="d-flex align-items-center d-md-block">
<ProfileAvatar
className="mb-md-3"
src={profileImage.src}
isDefault={profileImage.isDefault}
onSave={this.handleSaveProfilePhoto}
onDelete={this.handleDeleteProfilePhoto}
savePhotoState={this.props.savePhotoState}
isEditable={this.props.isAuthenticatedUserProfile && !requiresParentalConsent}
/>
</div>
</div>
<div className="col pl-0">
<div className="d-md-none">
{this.renderHeadingLockup()}
</div>
<div className="d-none d-md-block float-right">
{this.renderViewMyRecordsButton()}
</div>
</div>
</div>
{this.renderPhotoUploadErrorMessage()}
<div className="row">
<div className="col-md-4 col-lg-4">
<div className="d-none d-md-block mb-4">
{this.renderHeadingLockup()}
</div>
<div className="d-md-none mb-4">
{this.renderViewMyRecordsButton()}
</div>
<Name
name={name}
visibilityName={visibilityName}
formId="name"
{...commonFormProps}
/>
<Country
country={country}
visibilityCountry={visibilityCountry}
formId="country"
{...commonFormProps}
/>
<PreferredLanguage
languageProficiencies={languageProficiencies}
visibilityLanguageProficiencies={visibilityLanguageProficiencies}
formId="languageProficiencies"
{...commonFormProps}
/>
<Education
levelOfEducation={levelOfEducation}
visibilityLevelOfEducation={visibilityLevelOfEducation}
formId="levelOfEducation"
{...commonFormProps}
/>
<SocialLinks
socialLinks={socialLinks}
draftSocialLinksByPlatform={draftSocialLinksByPlatform}
visibilitySocialLinks={visibilitySocialLinks}
formId="socialLinks"
{...commonFormProps}
/>
</div>
<div className="pt-md-3 col-md-8 col-lg-7 offset-lg-1">
{shouldShowAgeMessage ? <AgeMessage accountURL="#account" /> : null}
<Bio
bio={bio}
visibilityBio={visibilityBio}
formId="bio"
{...commonFormProps}
/>
<Certificates
visibilityCourseCertificates={visibilityCourseCertificates}
formId="certificates"
{...commonFormProps}
/>
</div>
</div>
</div>
</div>
);
}
}
ProfilePage.propTypes = {
// Account data
username: PropTypes.string,
requiresParentalConsent: PropTypes.bool,
dateJoined: PropTypes.string,
isAuthenticatedUserProfile: PropTypes.bool.isRequired,
// Bio form data
bio: PropTypes.string,
visibilityBio: PropTypes.string.isRequired,
// Certificates form data
courseCertificates: PropTypes.arrayOf(PropTypes.shape({
title: PropTypes.string,
})),
visibilityCourseCertificates: PropTypes.string.isRequired,
// Country form data
country: PropTypes.string,
visibilityCountry: PropTypes.string.isRequired,
// Education form data
levelOfEducation: PropTypes.string,
visibilityLevelOfEducation: PropTypes.string.isRequired,
// Language proficiency form data
languageProficiencies: PropTypes.arrayOf(PropTypes.shape({
code: PropTypes.string.isRequired,
})),
visibilityLanguageProficiencies: PropTypes.string.isRequired,
// Name form data
name: PropTypes.string,
visibilityName: PropTypes.string.isRequired,
// Social links form data
socialLinks: PropTypes.arrayOf(PropTypes.shape({
platform: PropTypes.string,
socialLink: PropTypes.string,
})),
draftSocialLinksByPlatform: PropTypes.objectOf(PropTypes.shape({
platform: PropTypes.string,
socialLink: PropTypes.string,
})),
visibilitySocialLinks: PropTypes.string.isRequired,
// Other data we need
profileImage: PropTypes.shape({
src: PropTypes.string,
isDefault: PropTypes.bool,
}),
saveState: PropTypes.oneOf([null, 'pending', 'complete', 'error']),
savePhotoState: PropTypes.oneOf([null, 'pending', 'complete', 'error']),
isLoadingProfile: PropTypes.bool.isRequired,
// Page state helpers
photoUploadError: PropTypes.objectOf(PropTypes.string),
// Actions
fetchProfile: PropTypes.func.isRequired,
saveProfile: PropTypes.func.isRequired,
saveProfilePhoto: PropTypes.func.isRequired,
deleteProfilePhoto: PropTypes.func.isRequired,
openForm: PropTypes.func.isRequired,
closeForm: PropTypes.func.isRequired,
updateDraft: PropTypes.func.isRequired,
// Router
match: PropTypes.shape({
params: PropTypes.shape({
username: PropTypes.string.isRequired,
}).isRequired,
}).isRequired,
// i18n
intl: intlShape.isRequired,
};
ProfilePage.defaultProps = {
saveState: null,
savePhotoState: null,
photoUploadError: {},
profileImage: {},
name: null,
username: null,
levelOfEducation: null,
country: null,
socialLinks: [],
draftSocialLinksByPlatform: {},
bio: null,
languageProficiencies: [],
courseCertificates: null,
requiresParentalConsent: null,
dateJoined: null,
};
export default connect(
profilePageSelector,
{
fetchProfile,
saveProfilePhoto,
deleteProfilePhoto,
saveProfile,
openForm,
closeForm,
updateDraft,
},
)(injectIntl(ProfilePage));

View File

@@ -1,11 +0,0 @@
import { defineMessages } from 'react-intl';
const messages = defineMessages({
'profile.viewMyRecords': {
id: 'profile.viewMyRecords',
defaultMessage: 'View My Records',
description: 'A link to go view my academic records',
},
});
export default messages;

View File

@@ -1,113 +0,0 @@
/* eslint-disable global-require */
import React from 'react';
import { mount } from 'enzyme';
import renderer from 'react-test-renderer';
import { Provider } from 'react-redux';
import { IntlProvider } from 'react-intl';
import configureMockStore from 'redux-mock-store';
import * as analytics from '@edx/frontend-analytics';
import ConnectedProfilePage from './ProfilePage';
const mockStore = configureMockStore();
const storeMocks = {
loadingApp: require('./__mocks__/loadingApp.mockStore.js'),
viewOwnProfile: require('./__mocks__/viewOwnProfile.mockStore.js'),
viewOtherProfile: require('./__mocks__/viewOtherProfile.mockStore.js'),
savingEditedBio: require('./__mocks__/savingEditedBio.mockStore.js'),
};
const requiredProfilePageProps = {
isAuthenticatedUserProfile: true,
fetchProfile: () => {},
saveProfile: () => {},
saveProfilePhoto: () => {},
deleteProfilePhoto: () => {},
openField: () => {},
closeField: () => {},
match: { params: { username: 'staff' } },
};
describe('<ProfilePage />', () => {
describe('Renders correctly in various states', () => {
it('app loading', () => {
analytics.sendTrackingLogEvent = jest.fn();
const tree = renderer
.create((
<IntlProvider locale="en">
<Provider store={mockStore(storeMocks.loadingApp)}>
<ConnectedProfilePage {...requiredProfilePageProps} />
</Provider>
</IntlProvider>
))
.toJSON();
expect(tree).toMatchSnapshot();
});
it('viewing own profile', () => {
analytics.sendTrackingLogEvent = jest.fn();
const tree = renderer
.create((
<IntlProvider locale="en">
<Provider store={mockStore(storeMocks.viewOwnProfile)}>
<ConnectedProfilePage {...requiredProfilePageProps} />
</Provider>
</IntlProvider>
))
.toJSON();
expect(tree).toMatchSnapshot();
});
it('viewing other profile', () => {
analytics.sendTrackingLogEvent = jest.fn();
const tree = renderer
.create((
<IntlProvider locale="en">
<Provider store={mockStore(storeMocks.viewOtherProfile)}>
<ConnectedProfilePage {...requiredProfilePageProps} />
</Provider>
</IntlProvider>
))
.toJSON();
expect(tree).toMatchSnapshot();
});
it('while saving an edited bio', () => {
analytics.sendTrackingLogEvent = jest.fn();
const tree = renderer
.create((
<IntlProvider locale="en">
<Provider store={mockStore(storeMocks.savingEditedBio)}>
<ConnectedProfilePage {...requiredProfilePageProps} />
</Provider>
</IntlProvider>
))
.toJSON();
expect(tree).toMatchSnapshot();
});
});
describe('handles analytics', () => {
it('calls sendTrackingLogEvent when mounting', () => {
analytics.sendTrackingLogEvent = jest.fn();
mount((
<IntlProvider locale="en">
<Provider store={mockStore(storeMocks.loadingApp)}>
<ConnectedProfilePage
{...requiredProfilePageProps}
match={{ params: { username: 'test-username' } }}
/>
</Provider>
</IntlProvider>
));
expect(analytics.sendTrackingLogEvent.mock.calls.length).toBe(1);
expect(analytics.sendTrackingLogEvent.mock.calls[0][0]).toEqual('edx.profile.viewed');
expect(analytics.sendTrackingLogEvent.mock.calls[0][1]).toEqual({
username: 'test-username',
});
});
});
});

View File

@@ -1,42 +0,0 @@
import React from 'react';
import { StatusAlert } from '@edx/paragon';
import { FormattedMessage } from 'react-intl';
import { configuration } from '../../config/environment';
const { ACCOUNT_SETTINGS_URL } = configuration;
function AgeMessage() {
return (
<StatusAlert
alertType="info"
dialog={
<React.Fragment>
<FormattedMessage
id="profile.age.headline"
defaultMessage="Your profile cannot be shared."
description="error message"
tagName="h6"
/>
<FormattedMessage
id="profile.age.details"
defaultMessage="To share your profile with other edX learners, you must confirm that you are over the age of 13."
description="error message"
tagName="p"
/>
<a href={ACCOUNT_SETTINGS_URL}>
<FormattedMessage
id="profile.age.set.date"
defaultMessage="Set your date of birth"
description="label on a link to set birthday"
/>
</a>
</React.Fragment>
}
dismissible={false}
open
/>
);
}
export default AgeMessage;

View File

@@ -1,157 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { FormattedMessage } from 'react-intl';
import { injectIntl, intlShape } from '@edx/frontend-i18n'; // eslint-disable-line
import { ValidationFormGroup } from '@edx/paragon';
import messages from './Bio.messages';
// Components
import FormControls from './elements/FormControls';
import EditableItemHeader from './elements/EditableItemHeader';
import EmptyContent from './elements/EmptyContent';
import SwitchContent from './elements/SwitchContent';
// Selectors
import { editableFormSelector } from '../../selectors/ProfilePageSelector';
class Bio extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
this.handleClose = this.handleClose.bind(this);
this.handleOpen = this.handleOpen.bind(this);
}
handleChange(e) {
const { name, value } = e.target;
this.props.changeHandler(name, value);
}
handleSubmit(e) {
e.preventDefault();
this.props.submitHandler(this.props.formId);
}
handleClose() {
this.props.closeHandler(this.props.formId);
}
handleOpen() {
this.props.openHandler(this.props.formId);
}
render() {
const {
formId, bio, visibilityBio, editMode, saveState, error, intl,
} = this.props;
return (
<SwitchContent
className="mb-5"
expression={editMode}
cases={{
editing: (
<div role="dialog" aria-labelledby={`${formId}-label`}>
<form onSubmit={this.handleSubmit}>
<ValidationFormGroup
for={formId}
invalid={error !== null}
invalidMessage={error}
>
<label className="edit-section-header" htmlFor={formId}>
{intl.formatMessage(messages['profile.bio.about.me'])}
</label>
<textarea
className="form-control"
id={formId}
name={formId}
value={bio}
onChange={this.handleChange}
/>
</ValidationFormGroup>
<FormControls
visibilityId="visibilityBio"
saveState={saveState}
visibility={visibilityBio}
cancelHandler={this.handleClose}
changeHandler={this.handleChange}
/>
</form>
</div>
),
editable: (
<React.Fragment>
<EditableItemHeader
content={intl.formatMessage(messages['profile.bio.about.me'])}
showEditButton
onClickEdit={this.handleOpen}
showVisibility={visibilityBio !== null}
visibility={visibilityBio}
/>
<p className="lead">{bio}</p>
</React.Fragment>
),
empty: (
<React.Fragment>
<EditableItemHeader content={intl.formatMessage(messages['profile.bio.about.me'])} />
<EmptyContent onClick={this.handleOpen}>
<FormattedMessage
id="profile.bio.empty"
defaultMessage="Add a short bio"
description="instructions when the user hasn't written an About Me"
/>
</EmptyContent>
</React.Fragment>
),
static: (
<React.Fragment>
<EditableItemHeader content={intl.formatMessage(messages['profile.bio.about.me'])} />
<p className="lead">{bio}</p>
</React.Fragment>
),
}}
/>
);
}
}
Bio.propTypes = {
// It'd be nice to just set this as a defaultProps...
// except the class that comes out on the other side of react-redux's
// connect() method won't have it anymore. Static properties won't survive
// through the higher order function.
formId: PropTypes.string.isRequired,
// From Selector
bio: PropTypes.string,
visibilityBio: PropTypes.oneOf(['private', 'all_users']),
editMode: PropTypes.oneOf(['editing', 'editable', 'empty', 'static']),
saveState: PropTypes.string,
error: PropTypes.string,
// Actions
changeHandler: PropTypes.func.isRequired,
submitHandler: PropTypes.func.isRequired,
closeHandler: PropTypes.func.isRequired,
openHandler: PropTypes.func.isRequired,
// i18n
intl: intlShape.isRequired,
};
Bio.defaultProps = {
editMode: 'static',
saveState: null,
bio: null,
visibilityBio: 'private',
error: null,
};
export default connect(
editableFormSelector,
{},
)(injectIntl(Bio));

View File

@@ -1,11 +0,0 @@
import { defineMessages } from 'react-intl';
const messages = defineMessages({
'profile.bio.about.me': {
id: 'profile.bio.about.me',
defaultMessage: 'About Me',
description: 'A section of a user profile',
},
});
export default messages;

View File

@@ -1,228 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { FormattedDate, FormattedMessage } from 'react-intl';
import { injectIntl, intlShape } from '@edx/frontend-i18n'; // eslint-disable-line
import { Hyperlink } from '@edx/paragon';
import { connect } from 'react-redux';
import get from 'lodash.get';
import messages from './Certificates.messages';
// Components
import FormControls from './elements/FormControls';
import EditableItemHeader from './elements/EditableItemHeader';
import SwitchContent from './elements/SwitchContent';
// Assets
import microMastersSVG from '../../../assets/micro-masters.svg';
import professionalCertificateSVG from '../../../assets/professional-certificate.svg';
import verifiedCertificateSVG from '../../../assets/verified-certificate.svg';
// Selectors
import { certificatesSelector } from '../../selectors/ProfilePageSelector';
class Certificates extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
this.handleClose = this.handleClose.bind(this);
this.handleOpen = this.handleOpen.bind(this);
}
handleChange(e) {
const { name, value } = e.target;
this.props.changeHandler(name, value);
}
handleSubmit(e) {
e.preventDefault();
this.props.submitHandler(this.props.formId);
}
handleClose() {
this.props.closeHandler(this.props.formId);
}
handleOpen() {
this.props.openHandler(this.props.formId);
}
renderCertificate({
certificateType, courseDisplayName, courseOrganization, modifiedDate, downloadUrl,
}) {
const { intl } = this.props;
const certificateIllustration = ((type) => {
switch (type) {
case 'Professional Certificate':
return professionalCertificateSVG;
case 'MicroMasters Certificate':
return microMastersSVG;
case 'Verified Certificate':
return verifiedCertificateSVG;
default:
return null;
}
})(certificateType);
return (
<div key={downloadUrl} className="col col-sm-6 d-flex align-items-stretch">
<div className="card mb-4 certificate flex-grow-1">
<div
className="certificate-type-illustration"
style={{ backgroundImage: `url(${certificateIllustration})` }}
/>
<div className="card-body d-flex flex-column">
<div className="card-title">
<p className="small mb-0">
{intl.formatMessage(get(
messages,
`profile.certificates.types.${certificateType}`,
messages['profile.certificates.types.unknown'],
))}
</p>
<h4 className="certificate-title">{courseDisplayName}</h4>
</div>
<p className="small mb-0">
<FormattedMessage
id="profile.certificate.organization.label"
defaultMessage="From"
/>
</p>
<p className="h6 mb-4">{courseOrganization}</p>
<div className="flex-grow-1" />
<p className="small mb-2">
<FormattedMessage
id="profile.certificate.completion.date.label"
defaultMessage="Completed on {date}"
values={{
date: <FormattedDate value={new Date(modifiedDate)} />,
}}
/>
</p>
<div>
<Hyperlink href={downloadUrl} className="btn btn-outline-primary" target="_blank">
{intl.formatMessage(messages['profile.certificates.view.certificate'])}
</Hyperlink>
</div>
</div>
</div>
</div>
);
}
renderCertificates() {
if (this.props.certificates === null || this.props.certificates.length === 0) {
return (<FormattedMessage
id="profile.no.certificates"
defaultMessage="You don't have any certificates yet."
description="displays when user has no course completion certificates"
/>);
}
return (
<div className="row align-items-stretch">{this.props.certificates.map(certificate => this.renderCertificate(certificate))}</div>
);
}
render() {
const {
visibilityCourseCertificates, editMode, saveState, intl,
} = this.props;
return (
<SwitchContent
className="mb-4"
expression={editMode}
cases={{
editing: (
<div role="dialog" aria-labelledby="course-certificates-label">
<form onSubmit={this.handleSubmit}>
<EditableItemHeader
headingId="course-certificates-label"
content={intl.formatMessage(messages['profile.certificates.my.certificates'])}
/>
<FormControls
visibilityId="visibilityCourseCertificates"
saveState={saveState}
visibility={visibilityCourseCertificates}
cancelHandler={this.handleClose}
changeHandler={this.handleChange}
/>
{this.renderCertificates()}
</form>
</div>
),
editable: (
<React.Fragment>
<EditableItemHeader
content={intl.formatMessage(messages['profile.certificates.my.certificates'])}
showEditButton
onClickEdit={this.handleOpen}
showVisibility={visibilityCourseCertificates !== null}
visibility={visibilityCourseCertificates}
/>
{this.renderCertificates()}
</React.Fragment>
),
empty: (
<React.Fragment>
<EditableItemHeader
content={intl.formatMessage(messages['profile.certificates.my.certificates'])}
showEditButton
onClickEdit={this.handleOpen}
showVisibility={visibilityCourseCertificates !== null}
visibility={visibilityCourseCertificates}
/>
{this.renderCertificates()}
</React.Fragment>
),
static: (
<React.Fragment>
<EditableItemHeader content={intl.formatMessage(messages['profile.certificates.my.certificates'])} />
{this.renderCertificates()}
</React.Fragment>
),
}}
/>
);
}
}
Certificates.propTypes = {
// It'd be nice to just set this as a defaultProps...
// except the class that comes out on the other side of react-redux's
// connect() method won't have it anymore. Static properties won't survive
// through the higher order function.
formId: PropTypes.string.isRequired,
// From Selector
certificates: PropTypes.arrayOf(PropTypes.shape({
title: PropTypes.string,
})),
visibilityCourseCertificates: PropTypes.oneOf(['private', 'all_users']),
editMode: PropTypes.oneOf(['editing', 'editable', 'empty', 'static']),
saveState: PropTypes.string,
// Actions
changeHandler: PropTypes.func.isRequired,
submitHandler: PropTypes.func.isRequired,
closeHandler: PropTypes.func.isRequired,
openHandler: PropTypes.func.isRequired,
// i18n
intl: intlShape.isRequired,
};
Certificates.defaultProps = {
editMode: 'static',
saveState: null,
visibilityCourseCertificates: 'private',
certificates: null,
};
export default connect(
certificatesSelector,
{},
)(injectIntl(Certificates));

View File

@@ -1,31 +0,0 @@
import { defineMessages } from 'react-intl';
const messages = defineMessages({
'profile.certificates.my.certificates': {
id: 'profile.certificates.my.certificates',
defaultMessage: 'My Certificates',
description: 'A section of a user profile',
},
'profile.certificates.view.certificate': {
id: 'profile.certificates.view.certificate',
defaultMessage: 'View Certificate',
description: 'A call to action to view a certificate',
},
'profile.certificates.types.verified': {
id: 'profile.certificates.types.verified',
defaultMessage: 'Verified Certificate',
description: 'A type of certificate a user may have earned',
},
'profile.certificates.types.professional': {
id: 'profile.certificates.types.professional',
defaultMessage: 'Professional Certificate',
description: 'A type of certificate a user may have earned',
},
'profile.certificates.types.unknown': {
id: 'profile.certificates.types.unknown',
defaultMessage: 'Certificate',
description: 'The string to display when a certificate is of an unknown type',
},
});
export default messages;

View File

@@ -1,177 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { injectIntl, intlShape } from '@edx/frontend-i18n'; // eslint-disable-line
import { ValidationFormGroup } from '@edx/paragon';
import messages from './Country.messages';
// Components
import FormControls from './elements/FormControls';
import EditableItemHeader from './elements/EditableItemHeader';
import EmptyContent from './elements/EmptyContent';
import SwitchContent from './elements/SwitchContent';
// Selectors
import { countrySelector } from '../../selectors/ProfilePageSelector';
class Country extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
this.handleClose = this.handleClose.bind(this);
this.handleOpen = this.handleOpen.bind(this);
}
handleChange(e) {
const {
name,
value,
} = e.target;
this.props.changeHandler(name, value);
}
handleSubmit(e) {
e.preventDefault();
this.props.submitHandler(this.props.formId);
}
handleClose() {
this.props.closeHandler(this.props.formId);
}
handleOpen() {
this.props.openHandler(this.props.formId);
}
render() {
const {
formId,
country,
visibilityCountry,
editMode,
saveState,
error,
intl,
sortedCountries,
countryMessages,
} = this.props;
return (
<SwitchContent
className="mb-5"
expression={editMode}
cases={{
editing: (
<div role="dialog" aria-labelledby={`${formId}-label`}>
<form onSubmit={this.handleSubmit}>
<ValidationFormGroup
for={formId}
invalid={error !== null}
invalidMessage={error}
>
<label className="edit-section-header" htmlFor={formId}>
{intl.formatMessage(messages['profile.country.label'])}
</label>
<select
className="form-control"
type="select"
id={formId}
name={formId}
value={country}
onChange={this.handleChange}
>
{sortedCountries.map(({ code, name }) => (
<option key={code} value={code}>{name}</option>
))}
</select>
</ValidationFormGroup>
<FormControls
visibilityId="visibilityCountry"
saveState={saveState}
visibility={visibilityCountry}
cancelHandler={this.handleClose}
changeHandler={this.handleChange}
/>
</form>
</div>
),
editable: (
<React.Fragment>
<EditableItemHeader
content={intl.formatMessage(messages['profile.country.label'])}
showEditButton
onClickEdit={this.handleOpen}
showVisibility={visibilityCountry !== null}
visibility={visibilityCountry}
/>
<p className="h5">{countryMessages[country]}</p>
</React.Fragment>
),
empty: (
<React.Fragment>
<EditableItemHeader
content={intl.formatMessage(messages['profile.country.label'])}
/>
<EmptyContent onClick={this.handleOpen}>
{intl.formatMessage(messages['profile.country.empty'])}
</EmptyContent>
</React.Fragment>
),
static: (
<React.Fragment>
<EditableItemHeader
content={intl.formatMessage(messages['profile.country.label'])}
/>
<p className="h5">{countryMessages[country]}</p>
</React.Fragment>
),
}}
/>
);
}
}
Country.propTypes = {
// It'd be nice to just set this as a defaultProps...
// except the class that comes out on the other side of react-redux's
// connect() method won't have it anymore. Static properties won't survive
// through the higher order function.
formId: PropTypes.string.isRequired,
// From Selector
country: PropTypes.string,
visibilityCountry: PropTypes.oneOf(['private', 'all_users']),
editMode: PropTypes.oneOf(['editing', 'editable', 'empty', 'static']),
saveState: PropTypes.string,
error: PropTypes.string,
sortedCountries: PropTypes.arrayOf(PropTypes.shape({
code: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
})).isRequired,
countryMessages: PropTypes.objectOf(PropTypes.string).isRequired,
// Actions
changeHandler: PropTypes.func.isRequired,
submitHandler: PropTypes.func.isRequired,
closeHandler: PropTypes.func.isRequired,
openHandler: PropTypes.func.isRequired,
// i18n
intl: intlShape.isRequired,
};
Country.defaultProps = {
editMode: 'static',
saveState: null,
country: null,
visibilityCountry: 'private',
error: null,
};
export default connect(
countrySelector,
{},
)(injectIntl(Country));

View File

@@ -1,16 +0,0 @@
import { defineMessages } from 'react-intl';
const messages = defineMessages({
'profile.country.label': {
id: 'profile.country.label',
defaultMessage: 'Location',
description: 'The label for a country in a user profile.',
},
'profile.country.empty': {
id: 'profile.country.empty',
defaultMessage: 'Add location',
description: 'The affordance to add country location to a users profile.',
},
});
export default messages;

View File

@@ -1,29 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage, FormattedDate } from 'react-intl';
function DateJoined({ date }) {
if (date == null) return null;
return (
<p className="mb-0">
<FormattedMessage
id="profile.datejoined.member.since"
defaultMessage="Member since {year}"
description="A label for how long the user has been a member"
values={{
year: <FormattedDate value={new Date(date)} year="numeric" />,
}}
/>
</p>
);
}
DateJoined.propTypes = {
date: PropTypes.string,
};
DateJoined.defaultProps = {
date: null,
};
export default DateJoined;

View File

@@ -1,186 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { FormattedMessage } from 'react-intl';
import { injectIntl, intlShape } from '@edx/frontend-i18n'; // eslint-disable-line
import get from 'lodash.get';
import { ValidationFormGroup } from '@edx/paragon';
import messages from './Education.messages';
// Components
import FormControls from './elements/FormControls';
import EditableItemHeader from './elements/EditableItemHeader';
import EmptyContent from './elements/EmptyContent';
import SwitchContent from './elements/SwitchContent';
// Constants
import EDUCATION_LEVELS from '../../constants/education';
// Selectors
import { editableFormSelector } from '../../selectors/ProfilePageSelector';
class Education extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
this.handleClose = this.handleClose.bind(this);
this.handleOpen = this.handleOpen.bind(this);
}
handleChange(e) {
const {
name,
value,
} = e.target;
this.props.changeHandler(name, value);
}
handleSubmit(e) {
e.preventDefault();
this.props.submitHandler(this.props.formId);
}
handleClose() {
this.props.closeHandler(this.props.formId);
}
handleOpen() {
this.props.openHandler(this.props.formId);
}
render() {
const {
formId, levelOfEducation, visibilityLevelOfEducation, editMode, saveState, error, intl,
} = this.props;
return (
<SwitchContent
className="mb-5"
expression={editMode}
cases={{
editing: (
<div role="dialog" aria-labelledby={`${formId}-label`}>
<form onSubmit={this.handleSubmit}>
<ValidationFormGroup
for={formId}
invalid={error !== null}
invalidMessage={error}
>
<label className="edit-section-header" htmlFor={formId}>
{intl.formatMessage(messages['profile.education.education'])}
</label>
<select
className="form-control"
id={formId}
name={formId}
value={levelOfEducation}
onChange={this.handleChange}
>
{EDUCATION_LEVELS.map(level => (
<option key={level} value={level}>
{intl.formatMessage(get(
messages,
`profile.education.levels.${level}`,
messages['profile.education.levels.o'],
))}
</option>
))}
</select>
</ValidationFormGroup>
<FormControls
visibilityId="visibilityLevelOfEducation"
saveState={saveState}
visibility={visibilityLevelOfEducation}
cancelHandler={this.handleClose}
changeHandler={this.handleChange}
/>
</form>
</div>
),
editable: (
<React.Fragment>
<EditableItemHeader
content={intl.formatMessage(messages['profile.education.education'])}
showEditButton
onClickEdit={this.handleOpen}
showVisibility={visibilityLevelOfEducation !== null}
visibility={visibilityLevelOfEducation}
/>
<p className="h5">
{intl.formatMessage(get(
messages,
`profile.education.levels.${levelOfEducation}`,
messages['profile.education.levels.o'],
))}
</p>
</React.Fragment>
),
empty: (
<React.Fragment>
<EditableItemHeader content={intl.formatMessage(messages['profile.education.education'])} />
<EmptyContent onClick={this.handleOpen}>
<FormattedMessage
id="profile.education.empty"
defaultMessage="Add education"
description="instructions when the user doesn't have their level of education set"
/>
</EmptyContent>
</React.Fragment>
),
static: (
<React.Fragment>
<EditableItemHeader content={intl.formatMessage(messages['profile.education.education'])} />
<p className="h5">
{intl.formatMessage(get(
messages,
`profile.education.levels.${levelOfEducation}`,
messages['profile.education.levels.o'],
))}
</p>
</React.Fragment>
),
}}
/>
);
}
}
Education.propTypes = {
// It'd be nice to just set this as a defaultProps...
// except the class that comes out on the other side of react-redux's
// connect() method won't have it anymore. Static properties won't survive
// through the higher order function.
formId: PropTypes.string.isRequired,
// From Selector
levelOfEducation: PropTypes.string,
visibilityLevelOfEducation: PropTypes.oneOf(['private', 'all_users']),
editMode: PropTypes.oneOf(['editing', 'editable', 'empty', 'static']),
saveState: PropTypes.string,
error: PropTypes.string,
// Actions
changeHandler: PropTypes.func.isRequired,
submitHandler: PropTypes.func.isRequired,
closeHandler: PropTypes.func.isRequired,
openHandler: PropTypes.func.isRequired,
// i18n
intl: intlShape.isRequired,
};
Education.defaultProps = {
editMode: 'static',
saveState: null,
levelOfEducation: null,
visibilityLevelOfEducation: 'private',
error: null,
};
export default connect(
editableFormSelector,
{},
)(injectIntl(Education));

View File

@@ -1,56 +0,0 @@
import { defineMessages } from 'react-intl';
const messages = defineMessages({
'profile.education.education': {
id: 'profile.education.education',
defaultMessage: 'Education',
description: 'A section of a user profile',
},
'profile.education.levels.p': {
id: 'profile.education.levels.p',
defaultMessage: 'Doctorate',
description: 'Selected by the user if their highest level of education is a doctorate degree.',
},
'profile.education.levels.m': {
id: 'profile.education.levels.m',
defaultMessage: "Master's or professional degree",
description: "Selected by the user if their highest level of education is a master's or professional degree from a college or university.",
},
'profile.education.levels.b': {
id: 'profile.education.levels.b',
defaultMessage: "Bachelor's Degree",
description: "Selected by the user if their highest level of education is a four year college or university bachelor's degree.",
},
'profile.education.levels.a': {
id: 'profile.education.levels.a',
defaultMessage: "Associate's degree",
description: "Selected by the user if their highest level of education is an associate's degree. 1-2 years of college or university.",
},
'profile.education.levels.hs': {
id: 'profile.education.levels.hs',
defaultMessage: 'Secondary/high school',
description: 'Selected by the user if their highest level of education is secondary or high school. 9-12 years of education.',
},
'profile.education.levels.jhs': {
id: 'profile.education.levels.jhs',
defaultMessage: 'Junior secondary/junior high/middle school',
description: 'Selected by the user if their highest level of education is junior or middle school. 6-8 years of education.',
},
'profile.education.levels.el': {
id: 'profile.education.levels.el',
defaultMessage: 'Elementary/primary school',
description: 'Selected by the user if their highest level of education is elementary or primary school. 1-5 years of education.',
},
'profile.education.levels.none': {
id: 'profile.education.levels.none',
defaultMessage: 'No formal education',
description: 'Selected by the user to describe their education.',
},
'profile.education.levels.o': {
id: 'profile.education.levels.o',
defaultMessage: 'Other education',
description: 'Selected by the user if they have a type of education not described by the other choices.',
},
});
export default messages;

View File

@@ -1,157 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { injectIntl, intlShape } from '@edx/frontend-i18n'; // eslint-disable-line
import messages from './Name.messages';
// Components
import FormControls from './elements/FormControls';
import EditableItemHeader from './elements/EditableItemHeader';
import EmptyContent from './elements/EmptyContent';
import SwitchContent from './elements/SwitchContent';
// Selectors
import { editableFormSelector } from '../../selectors/ProfilePageSelector';
class Name extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
this.handleClose = this.handleClose.bind(this);
this.handleOpen = this.handleOpen.bind(this);
}
handleChange(e) {
const {
name,
value,
} = e.target;
this.props.changeHandler(name, value);
}
handleSubmit(e) {
e.preventDefault();
this.props.submitHandler(this.props.formId);
}
handleClose() {
this.props.closeHandler(this.props.formId);
}
handleOpen() {
this.props.openHandler(this.props.formId);
}
render() {
const {
formId, name, visibilityName, editMode, saveState, intl,
} = this.props;
return (
<SwitchContent
className="mb-5"
expression={editMode}
cases={{
editing: (
<div role="dialog" aria-labelledby={`${formId}-label`}>
<form onSubmit={this.handleSubmit}>
<div className="form-group">
<EditableItemHeader content={intl.formatMessage(messages['profile.name.full.name'])} />
{/*
This isn't a mistake - the name field should not be editable. But if it were,
you'd find the original code got deleted in the commit which added this comment.
-djoy
TODO: Relatedly, the plumbing for editing the name field is still in place.
Once we're super sure we don't want it back, you could delete the name props and
such to fully get rid of it.
*/}
<p className="h5">{name}</p>
<small className="form-text text-muted" id={`${formId}-help-text`}>
{intl.formatMessage(messages['profile.name.details'])}
</small>
</div>
<FormControls
visibilityId="visibilityName"
saveState={saveState}
visibility={visibilityName}
cancelHandler={this.handleClose}
changeHandler={this.handleChange}
/>
</form>
</div>
),
editable: (
<React.Fragment>
<EditableItemHeader
content={intl.formatMessage(messages['profile.name.full.name'])}
showEditButton
onClickEdit={this.handleOpen}
showVisibility={visibilityName !== null}
visibility={visibilityName}
/>
<p className="h5">{name}</p>
<small className="form-text text-muted">
{intl.formatMessage(messages['profile.name.details'])}
</small>
</React.Fragment>
),
empty: (
<React.Fragment>
<EditableItemHeader content={intl.formatMessage(messages['profile.name.full.name'])} />
<EmptyContent onClick={this.handleOpen}>
{intl.formatMessage(messages['profile.name.empty'])}
</EmptyContent>
<small className="form-text text-muted">
{intl.formatMessage(messages['profile.name.details'])}
</small>
</React.Fragment>
),
static: (
<React.Fragment>
<EditableItemHeader content={intl.formatMessage(messages['profile.name.full.name'])} />
<p className="h5">{name}</p>
</React.Fragment>
),
}}
/>
);
}
}
Name.propTypes = {
// It'd be nice to just set this as a defaultProps...
// except the class that comes out on the other side of react-redux's
// connect() method won't have it anymore. Static properties won't survive
// through the higher order function.
formId: PropTypes.string.isRequired,
// From Selector
name: PropTypes.string,
visibilityName: PropTypes.oneOf(['private', 'all_users']),
editMode: PropTypes.oneOf(['editing', 'editable', 'empty', 'static']),
saveState: PropTypes.string,
// Actions
changeHandler: PropTypes.func.isRequired,
submitHandler: PropTypes.func.isRequired,
closeHandler: PropTypes.func.isRequired,
openHandler: PropTypes.func.isRequired,
// i18n
intl: intlShape.isRequired,
};
Name.defaultProps = {
editMode: 'static',
saveState: null,
name: null,
visibilityName: 'private',
};
export default connect(
editableFormSelector,
{},
)(injectIntl(Name));

View File

@@ -1,21 +0,0 @@
import { defineMessages } from 'react-intl';
const messages = defineMessages({
'profile.name.full.name': {
id: 'profile.name.full.name',
defaultMessage: 'Full Name',
description: 'A section of a user profile',
},
'profile.name.details': {
id: 'profile.name.details',
defaultMessage: 'This is the name that appears in your account and on your certificates.',
description: 'Describes the area for a user to update their name.',
},
'profile.name.empty': {
id: 'profile.name.empty',
defaultMessage: 'Add name',
description: 'The affordance to add a name to a users profile.',
},
});
export default messages;

View File

@@ -1,191 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { injectIntl, intlShape } from '@edx/frontend-i18n'; // eslint-disable-line
import { ValidationFormGroup } from '@edx/paragon';
import messages from './PreferredLanguage.messages';
// Components
import FormControls from './elements/FormControls';
import EditableItemHeader from './elements/EditableItemHeader';
import EmptyContent from './elements/EmptyContent';
import SwitchContent from './elements/SwitchContent';
// Selectors
import { preferredLanguageSelector } from '../../selectors/ProfilePageSelector';
class PreferredLanguage extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
this.handleClose = this.handleClose.bind(this);
this.handleOpen = this.handleOpen.bind(this);
}
handleChange(e) {
const {
name,
value: rawValue,
} = e.target;
let value = rawValue;
// Restructure the data.
// We deconstruct our value prop in render() so this
// changes our data's shape back to match what came in
if (name === this.props.formId) {
value = [{ code: rawValue }];
}
this.props.changeHandler(name, value);
}
handleSubmit(e) {
e.preventDefault();
this.props.submitHandler(this.props.formId);
}
handleClose() {
this.props.closeHandler(this.props.formId);
}
handleOpen() {
this.props.openHandler(this.props.formId);
}
render() {
const {
formId,
languageProficiencies,
visibilityLanguageProficiencies,
editMode,
saveState,
error,
intl,
sortedLanguages,
languageMessages,
} = this.props;
const value = languageProficiencies.length ? languageProficiencies[0].code : '';
return (
<SwitchContent
className="mb-5"
expression={editMode}
cases={{
editing: (
<div role="dialog" aria-labelledby={`${formId}-label`}>
<form onSubmit={this.handleSubmit}>
<ValidationFormGroup
for={formId}
invalid={error !== null}
invalidMessage={error}
>
<label className="edit-section-header" htmlFor={formId}>
{intl.formatMessage(messages['profile.preferredlanguage.label'])}
</label>
<select
id={formId}
name={formId}
className="form-control"
value={value}
onChange={this.handleChange}
>
{sortedLanguages.map(({ code, name }) => (
<option key={code} value={code}>{name}</option>
))}
</select>
</ValidationFormGroup>
<FormControls
visibilityId="visibilityLanguageProficiencies"
saveState={saveState}
visibility={visibilityLanguageProficiencies}
cancelHandler={this.handleClose}
changeHandler={this.handleChange}
/>
</form>
</div>
),
editable: (
<React.Fragment>
<EditableItemHeader
content={intl.formatMessage(messages['profile.preferredlanguage.label'])}
showEditButton
onClickEdit={this.handleOpen}
showVisibility={visibilityLanguageProficiencies !== null}
visibility={visibilityLanguageProficiencies}
/>
<p className="h5">{languageMessages[value]}</p>
</React.Fragment>
),
empty: (
<React.Fragment>
<EditableItemHeader
content={intl.formatMessage(messages['profile.preferredlanguage.label'])}
/>
<EmptyContent onClick={this.handleOpen}>
{intl.formatMessage(messages['profile.preferredlanguage.empty'])}
</EmptyContent>
</React.Fragment>
),
static: (
<React.Fragment>
<EditableItemHeader
content={intl.formatMessage(messages['profile.preferredlanguage.label'])}
/>
<p className="h5">{languageMessages[value]}</p>
</React.Fragment>
),
}}
/>
);
}
}
PreferredLanguage.propTypes = {
// It'd be nice to just set this as a defaultProps...
// except the class that comes out on the other side of react-redux's
// connect() method won't have it anymore. Static properties won't survive
// through the higher order function.
formId: PropTypes.string.isRequired,
// From Selector
languageProficiencies: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.shape({ code: PropTypes.string })),
// TODO: ProfilePageSelector should supply null values
// instead of empty strings when no value exists
PropTypes.oneOf(['']),
]),
visibilityLanguageProficiencies: PropTypes.oneOf(['private', 'all_users']),
editMode: PropTypes.oneOf(['editing', 'editable', 'empty', 'static']),
saveState: PropTypes.string,
error: PropTypes.string,
sortedLanguages: PropTypes.arrayOf(PropTypes.shape({
code: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
})).isRequired,
languageMessages: PropTypes.objectOf(PropTypes.string).isRequired,
// Actions
changeHandler: PropTypes.func.isRequired,
submitHandler: PropTypes.func.isRequired,
closeHandler: PropTypes.func.isRequired,
openHandler: PropTypes.func.isRequired,
// i18n
intl: intlShape.isRequired,
};
PreferredLanguage.defaultProps = {
editMode: 'static',
saveState: null,
languageProficiencies: [],
visibilityLanguageProficiencies: 'private',
error: null,
};
export default connect(
preferredLanguageSelector,
{},
)(injectIntl(PreferredLanguage));

View File

@@ -1,16 +0,0 @@
import { defineMessages } from 'react-intl';
const messages = defineMessages({
'profile.preferredlanguage.empty': {
id: 'profile.preferredlanguage.empty',
defaultMessage: 'Add language',
description: 'Instructions when the user doesnt have a preferred language set.',
},
'profile.preferredlanguage.label': {
id: 'profile.preferredlanguage.label',
defaultMessage: 'Primary Language Spoken',
description: 'The label for a users primary spoken language.',
},
});
export default messages;

View File

@@ -1,173 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Button, Dropdown } from '@edx/paragon';
import { FormattedMessage } from 'react-intl';
import { injectIntl, intlShape } from '@edx/frontend-i18n'; // eslint-disable-line
import { ReactComponent as DefaultAvatar } from '../../assets/avatar.svg';
import messages from './ProfileAvatar.messages';
class ProfileAvatar extends React.Component {
constructor(props) {
super(props);
this.fileInput = React.createRef();
this.form = React.createRef();
this.onClickUpload = this.onClickUpload.bind(this);
this.onClickDelete = this.onClickDelete.bind(this);
this.onChangeInput = this.onChangeInput.bind(this);
this.onSubmit = this.onSubmit.bind(this);
}
onClickUpload() {
this.fileInput.current.click();
}
onClickDelete() {
this.props.onDelete();
}
onChangeInput() {
this.onSubmit();
}
onSubmit(e) {
if (e) e.preventDefault();
this.props.onSave(new FormData(this.form.current));
this.form.current.reset();
}
renderPending() {
return (
<div
className="position-absolute w-100 h-100 d-flex justify-content-center align-items-center rounded-circle"
style={{ backgroundColor: 'rgba(0,0,0,.65)' }}
>
<div className="spinner-border text-primary" role="status" />
</div>
);
}
renderMenuContent() {
if (this.props.isDefault) {
return (
<Button
className="text-white btn-block btn-sm btn-link"
onClick={this.onClickUpload}
>
<FormattedMessage
id="profile.profileavatar.upload-button"
defaultMessage="Upload Photo"
description="Upload photo button"
/>
</Button>
);
}
return (
<Dropdown
buttonType="primary"
title={(
<FormattedMessage
id="profile.profileavatar.change-button"
defaultMessage="Change"
description="Change photo button"
/>
)}
menuItems={[
(
<button className="dropdown-item" onClick={this.onClickUpload}>
<FormattedMessage
id="profile.profileavatar.upload-button"
defaultMessage="Upload Photo"
description="Upload photo button"
/>
</button>
),
(
<button className="dropdown-item" onClick={this.onClickDelete}>
<FormattedMessage
id="profile.profileavatar.remove.button"
defaultMessage="Remove"
description="Remove photo button"
/>
</button>
),
]}
/>
);
}
renderMenu() {
if (!this.props.isEditable) return null;
return (
<div className="profile-avatar-menu-container">
{this.renderMenuContent()}
</div>
);
}
renderAvatar() {
const { intl } = this.props;
return this.props.isDefault ? (
<DefaultAvatar className="text-muted" role="img" aria-hidden focusable="false" viewBox="0 0 24 24" />
) : (
<img
className="w-100 h-100 d-block rounded-circle overflow-hidden"
style={{ objectFit: 'cover' }}
alt={intl.formatMessage(messages['profile.image.alt.attribute'])}
src={this.props.src}
/>
);
}
render() {
return (
<div className="profile-avatar-wrap position-relative">
<div className="profile-avatar rounded-circle bg-light">
{this.props.savePhotoState === 'pending' ? this.renderPending() : this.renderMenu() }
{this.renderAvatar()}
</div>
<form
ref={this.form}
onSubmit={this.onSubmit}
encType="multipart/form-data"
>
{/* The name of this input must be 'file' */}
<input
className="d-none form-control-file"
ref={this.fileInput}
type="file"
name="file"
id="photo-file"
onChange={this.onChangeInput}
accept=".jpg, .jpeg, .png"
/>
</form>
</div>
);
}
}
export default injectIntl(ProfileAvatar);
ProfileAvatar.propTypes = {
src: PropTypes.string,
isDefault: PropTypes.bool,
onSave: PropTypes.func.isRequired,
onDelete: PropTypes.func.isRequired,
savePhotoState: PropTypes.oneOf([null, 'pending', 'complete', 'error']),
isEditable: PropTypes.bool,
intl: intlShape.isRequired,
};
ProfileAvatar.defaultProps = {
src: null,
isDefault: true,
savePhotoState: null,
isEditable: false,
};

View File

@@ -1,11 +0,0 @@
import { defineMessages } from 'react-intl';
const messages = defineMessages({
'profile.image.alt.attribute': {
id: 'profile.image.alt.attribute',
defaultMessage: 'profile avatar',
description: 'Alt attribute for a profile photo',
},
});
export default messages;

View File

@@ -1,352 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { StatusAlert } from '@edx/paragon';
import { connect } from 'react-redux';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faTwitter, faFacebook, faLinkedin } from '@fortawesome/free-brands-svg-icons';
import { FormattedMessage } from 'react-intl';
import { injectIntl, intlShape } from '@edx/frontend-i18n'; // eslint-disable-line
import classNames from 'classnames';
import messages from './SocialLinks.messages';
// Components
import FormControls from './elements/FormControls';
import EditableItemHeader from './elements/EditableItemHeader';
import EmptyContent from './elements/EmptyContent';
import SwitchContent from './elements/SwitchContent';
// Selectors
import { editableFormSelector } from '../../selectors/ProfilePageSelector';
const platformDisplayInfo = {
facebook: {
icon: faFacebook,
name: 'Facebook',
},
twitter: {
icon: faTwitter,
name: 'Twitter',
},
linkedin: {
icon: faLinkedin,
name: 'LinkedIn',
},
};
class SocialLinks extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
this.handleClose = this.handleClose.bind(this);
this.handleOpen = this.handleOpen.bind(this);
}
handleChange(e) {
const { name, value } = e.target;
// The social links are a bit special. If we're updating them, we need to merge them
// with any existing social link drafts, essentially sending a fresh copy of the whole
// data structure back to the reducer. This helps the reducer stay simple and keeps
// special cases out of it, concentrating them here, where they began.
if (name !== 'visibilitySocialLinks') {
this.props.changeHandler(
'socialLinks',
this.mergeWithDrafts({
platform: name,
// If it's an empty string, send it as null.
// The empty string is just for the input. We want nulls.
socialLink: value,
}),
);
} else {
this.props.changeHandler(name, value);
}
}
mergeWithDrafts(newSocialLink) {
const knownPlatforms = ['twitter', 'facebook', 'linkedin'];
const updated = [];
knownPlatforms.forEach((platform) => {
if (newSocialLink.platform === platform) {
updated.push(newSocialLink);
} else if (this.props.draftSocialLinksByPlatform[platform] !== undefined) {
updated.push(this.props.draftSocialLinksByPlatform[platform]);
}
});
return updated;
}
handleSubmit(e) {
e.preventDefault();
this.props.submitHandler(this.props.formId);
}
handleClose() {
this.props.closeHandler(this.props.formId);
}
handleOpen() {
this.props.openHandler(this.props.formId);
}
render() {
const {
socialLinks, visibilitySocialLinks, editMode, saveState, error, intl,
} = this.props;
return (
<SwitchContent
className="mb-5"
expression={editMode}
cases={{
empty: (
<React.Fragment>
<EditableItemHeader content={intl.formatMessage(messages['profile.sociallinks.social.links'])} />
<ul className="list-unstyled">
{socialLinks.map(({ platform }) => (
<EmptyListItem
key={platform}
onClick={this.handleOpen}
name={platformDisplayInfo[platform].name}
/>
))}
</ul>
</React.Fragment>
),
static: (
<React.Fragment>
<EditableItemHeader
content={intl.formatMessage(messages['profile.sociallinks.social.links'])}
/>
<ul className="list-unstyled">
{socialLinks
.filter(({ socialLink }) => Boolean(socialLink))
.map(({ platform, socialLink }) => (
<StaticListItem
key={platform}
name={platformDisplayInfo[platform].name}
url={socialLink}
platform={platform}
/>
))
}
</ul>
</React.Fragment>
),
editable: (
<React.Fragment>
<EditableItemHeader
content={intl.formatMessage(messages['profile.sociallinks.social.links'])}
showEditButton
onClickEdit={this.handleOpen}
showVisibility={visibilitySocialLinks !== null}
visibility={visibilitySocialLinks}
/>
<ul className="list-unstyled">
{socialLinks.map(({ platform, socialLink }) => (
<EditableListItem
key={platform}
platform={platform}
name={platformDisplayInfo[platform].name}
url={socialLink}
onClickEmptyContent={this.handleOpen}
/>
))}
</ul>
</React.Fragment>
),
editing: (
<div role="dialog" aria-labelledby="social-links-label">
<form onSubmit={this.handleSubmit}>
<EditableItemHeader
headingId="social-links-label"
content={intl.formatMessage(messages['profile.sociallinks.social.links'])}
/>
{/* TODO: Replace this alert with per-field errors. Needs API update. */}
<div id="social-error-feedback">
{error !== null ? <StatusAlert alertType="danger" dialog={error} dismissible={false} open /> : null}
</div>
<ul className="list-unstyled">
{socialLinks.map(({ platform, socialLink }) => (
<EditingListItem
key={platform}
name={platformDisplayInfo[platform].name}
platform={platform}
value={socialLink}
/* TODO: Per-field errors: error={error !== null ? error[platform] : null} */
onChange={this.handleChange}
/>
))}
</ul>
<FormControls
visibilityId="visibilitySocialLinks"
saveState={saveState}
visibility={visibilitySocialLinks}
cancelHandler={this.handleClose}
changeHandler={this.handleChange}
/>
</form>
</div>
),
}}
/>
);
}
}
SocialLinks.propTypes = {
// It'd be nice to just set this as a defaultProps...
// except the class that comes out on the other side of react-redux's
// connect() method won't have it anymore. Static properties won't survive
// through the higher order function.
formId: PropTypes.string.isRequired,
// From Selector
socialLinks: PropTypes.arrayOf(PropTypes.shape({
platform: PropTypes.string,
socialLink: PropTypes.string,
})).isRequired,
draftSocialLinksByPlatform: PropTypes.objectOf(PropTypes.shape({
platform: PropTypes.string,
socialLink: PropTypes.string,
})),
visibilitySocialLinks: PropTypes.oneOf(['private', 'all_users']),
editMode: PropTypes.oneOf(['editing', 'editable', 'empty', 'static']),
saveState: PropTypes.string,
error: PropTypes.string,
// Actions
changeHandler: PropTypes.func.isRequired,
submitHandler: PropTypes.func.isRequired,
closeHandler: PropTypes.func.isRequired,
openHandler: PropTypes.func.isRequired,
// i18n
intl: intlShape.isRequired,
};
SocialLinks.defaultProps = {
editMode: 'static',
saveState: null,
draftSocialLinksByPlatform: {},
visibilitySocialLinks: 'private',
error: null,
};
export default connect(
editableFormSelector,
{},
)(injectIntl(SocialLinks));
function SocialLink({ url, name, platform }) {
return (
<a href={url} className="font-weight-bold">
<FontAwesomeIcon className="mr-2" icon={platformDisplayInfo[platform].icon} />
{name}
</a>
);
}
SocialLink.propTypes = {
url: PropTypes.string.isRequired,
platform: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
};
function EditableListItem({
url, platform, onClickEmptyContent, name,
}) {
const linkDisplay = url ? (
<SocialLink name={name} url={url} platform={platform} />
) : (
<EmptyContent onClick={onClickEmptyContent}>Add {name}</EmptyContent>
);
return <li className="form-group">{linkDisplay}</li>;
}
EditableListItem.propTypes = {
url: PropTypes.string,
platform: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
onClickEmptyContent: PropTypes.func,
};
EditableListItem.defaultProps = {
url: null,
onClickEmptyContent: null,
};
function EditingListItem({
platform, name, value, onChange, error,
}) {
return (
<li className="form-group">
<label htmlFor={`social-${platform}`}>{name}</label>
<input
className={classNames('form-control', { 'is-invalid': Boolean(error) })}
type="text"
id={`social-${platform}`}
name={platform}
value={value || ''}
onChange={onChange}
aria-describedby="social-error-feedback"
/>
</li>
);
}
EditingListItem.propTypes = {
platform: PropTypes.string.isRequired,
value: PropTypes.string,
name: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired,
error: PropTypes.string,
};
EditingListItem.defaultProps = {
value: null,
error: null,
};
function EmptyListItem({ onClick, name }) {
return (
<li className="mb-4">
<EmptyContent onClick={onClick}>
<FormattedMessage
id="profile.sociallinks.add"
defaultMessage="Add {network}"
values={{
network: name,
}}
description="{network} is the name of a social network such as Facebook or Twitter"
/>
</EmptyContent>
</li>
);
}
EmptyListItem.propTypes = {
name: PropTypes.string.isRequired,
onClick: PropTypes.func.isRequired,
};
function StaticListItem({ name, url, platform }) {
return (
<li className="mb-2">
<SocialLink name={name} url={url} platform={platform} />
</li>
);
}
StaticListItem.propTypes = {
name: PropTypes.string.isRequired,
url: PropTypes.string,
platform: PropTypes.string.isRequired,
};
StaticListItem.defaultProps = {
url: null,
};

View File

@@ -1,11 +0,0 @@
import { defineMessages } from 'react-intl';
const messages = defineMessages({
'profile.sociallinks.social.links': {
id: 'profile.sociallinks.social.links',
defaultMessage: 'Social Links',
description: 'A section of a user profile',
},
});
export default messages;

View File

@@ -1,40 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faPencilAlt } from '@fortawesome/free-solid-svg-icons';
import { injectIntl, intlShape } from '@edx/frontend-i18n'; // eslint-disable-line
import { Button } from '@edx/paragon';
import messages from './EditButton.messages';
function EditButton({
onClick, className, style, intl,
}) {
return (
<Button
className={classNames('btn-sm btn-link', className)}
onClick={onClick}
style={style}
>
<FontAwesomeIcon className="mr-1" icon={faPencilAlt} />
{intl.formatMessage(messages['profile.editbutton.edit'])}
</Button>
);
}
export default injectIntl(EditButton);
EditButton.propTypes = {
onClick: PropTypes.func.isRequired,
className: PropTypes.string,
style: PropTypes.object, // eslint-disable-line
// i18n
intl: intlShape.isRequired,
};
EditButton.defaultProps = {
className: null,
style: null,
};

View File

@@ -1,11 +0,0 @@
import { defineMessages } from 'react-intl';
const messages = defineMessages({
'profile.editbutton.edit': {
id: 'profile.editbutton.edit',
defaultMessage: 'Edit',
description: 'A button label',
},
});
export default messages;

View File

@@ -1,47 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import EditButton from './EditButton';
import { Visibility } from './Visibility';
function EditableItemHeader({
content,
showVisibility,
visibility,
showEditButton,
onClickEdit,
headingId,
}) {
return (
<React.Fragment>
<div className="editable-item-header mb-2">
<h2 className="edit-section-header" id={headingId}>
{content}
{showEditButton ? <EditButton style={{ marginTop: '-.35rem' }} className="float-right px-0" onClick={onClickEdit} /> : null}
</h2>
{showVisibility ? <p className="mb-0"><Visibility to={visibility} /></p> : null}
</div>
</React.Fragment>
);
}
export default EditableItemHeader;
EditableItemHeader.propTypes = {
onClickEdit: PropTypes.func,
showVisibility: PropTypes.bool,
showEditButton: PropTypes.bool,
content: PropTypes.node,
visibility: PropTypes.oneOf(['private', 'all_users']),
headingId: PropTypes.string,
};
EditableItemHeader.defaultProps = {
onClickEdit: () => {},
showVisibility: false,
showEditButton: false,
content: '',
visibility: 'private',
headingId: null,
};

View File

@@ -1,37 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faPlus } from '@fortawesome/free-solid-svg-icons';
function EmptyContent({ children, onClick, showPlusIcon }) {
return (
<div>
{onClick ? (
<button
className="pl-0 text-left btn btn-link"
onClick={onClick}
onKeyDown={(e) => { if (e.key === 'Enter') onClick(); }}
tabIndex={0}
>
{showPlusIcon ? <FontAwesomeIcon size="xs" className="mr-2" icon={faPlus} /> : null}
{children}
</button>
) : children}
</div>
);
}
export default EmptyContent;
EmptyContent.propTypes = {
onClick: PropTypes.func,
children: PropTypes.node,
showPlusIcon: PropTypes.bool,
};
EmptyContent.defaultProps = {
onClick: null,
children: null,
showPlusIcon: true,
};

View File

@@ -1,77 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Button, StatefulButton } from '@edx/paragon';
import { injectIntl, intlShape } from '@edx/frontend-i18n'; // eslint-disable-line
import messages from './FormControls.messages';
import { VisibilitySelect } from './Visibility';
function FormControls({
cancelHandler, changeHandler, visibility, visibilityId, saveState, intl,
}) {
// Eliminate error/failed state for save button
const buttonState = saveState === 'error' ? null : saveState;
return (
<div className="d-flex flex-row-reverse flex-wrap justify-content-end align-items-center">
<div className="form-group d-flex flex-wrap">
<label className="col-form-label" htmlFor={visibilityId}>
{intl.formatMessage(messages['profile.formcontrols.who.can.see'])}
</label>
<VisibilitySelect
id={visibilityId}
className="d-flex align-items-center"
type="select"
name={visibilityId}
value={visibility}
onChange={changeHandler}
/>
</div>
<div className="form-group flex-shrink-0 mr-auto">
<StatefulButton
type="submit"
className="btn-primary"
state={buttonState}
labels={{
default: intl.formatMessage(messages['profile.formcontrols.button.save']),
pending: intl.formatMessage(messages['profile.formcontrols.button.saving']),
complete: intl.formatMessage(messages['profile.formcontrols.button.saved']),
}}
onClick={(e) => {
// Swallow clicks if the state is pending.
// We do this instead of disabling the button to prevent
// it from losing focus (disabled elements cannot have focus).
// Disabling it would causes upstream issues in focus management.
// Swallowing the onSubmit event on the form would be better, but
// we would have to add that logic for every field given our
// current structure of the application.
if (buttonState === 'pending') e.preventDefault();
}}
disabledStates={[]}
/>
<Button className="btn-link" onClick={cancelHandler}>
{intl.formatMessage(messages['profile.formcontrols.button.cancel'])}
</Button>
</div>
</div>
);
}
export default injectIntl(FormControls);
FormControls.propTypes = {
saveState: PropTypes.oneOf([null, 'pending', 'complete', 'error']),
visibility: PropTypes.oneOf(['private', 'all_users']),
visibilityId: PropTypes.string.isRequired,
cancelHandler: PropTypes.func.isRequired,
changeHandler: PropTypes.func.isRequired,
// i18n
intl: intlShape.isRequired,
};
FormControls.defaultProps = {
visibility: 'private',
saveState: null,
};

View File

@@ -1,31 +0,0 @@
import { defineMessages } from 'react-intl';
const messages = defineMessages({
'profile.formcontrols.who.can.see': {
id: 'profile.formcontrols.who.can.see',
defaultMessage: 'Who can see this:',
description: 'What users can see this area?',
},
'profile.formcontrols.button.cancel': {
id: 'profile.formcontrols.button.cancel',
defaultMessage: 'Cancel',
description: 'A button label',
},
'profile.formcontrols.button.save': {
id: 'profile.formcontrols.button.save',
defaultMessage: 'Save',
description: 'A button label',
},
'profile.formcontrols.button.saving': {
id: 'profile.formcontrols.button.saving',
defaultMessage: 'Saving',
description: 'A button label',
},
'profile.formcontrols.button.saved': {
id: 'profile.formcontrols.button.saved',
defaultMessage: 'Saved',
description: 'A button label',
},
});
export default messages;

View File

@@ -1,65 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { TransitionReplace } from '@edx/paragon';
const onChildExit = (htmlNode) => {
// If the leaving child has focus, take control and redirect it
if (htmlNode.contains(document.activeElement)) {
// Get the newly entering sibling.
// It's the previousSibling, but not for any explicit reason. So checking for both.
const enteringChild = htmlNode.previousSibling || htmlNode.nextSibling;
// There's no replacement, do nothing.
if (!enteringChild) return;
// 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"])');
if (focusableElements.length) {
focusableElements[0].focus();
}
}
};
function SwitchContent({ expression, cases, className }) {
const getContent = (caseKey) => {
if (cases[caseKey]) {
if (typeof cases[caseKey] === 'string') {
return getContent(cases[caseKey]);
}
return React.cloneElement(cases[caseKey], { key: caseKey });
} else if (cases.default) {
if (typeof cases.default === 'string') {
return getContent(cases.default);
}
React.cloneElement(cases.default, { key: 'default' });
}
return null;
};
return (
<TransitionReplace
className={className}
onChildExit={onChildExit}
>
{getContent(expression)}
</TransitionReplace>
);
}
SwitchContent.propTypes = {
expression: PropTypes.string,
cases: PropTypes.objectOf(PropTypes.node).isRequired,
className: PropTypes.string,
};
SwitchContent.defaultProps = {
expression: null,
className: null,
};
export default SwitchContent;

View File

@@ -1,80 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { injectIntl, intlShape } from '@edx/frontend-i18n'; // eslint-disable-line
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faEyeSlash, faEye } from '@fortawesome/free-regular-svg-icons';
import messages from './Visibility.messages';
function Visibility({ to, intl }) {
const icon = to === 'private' ? faEyeSlash : faEye;
const label = to === 'private' ?
intl.formatMessage(messages['profile.visibility.who.just.me']) :
intl.formatMessage(messages['profile.visibility.who.everyone']);
return (
<span className="ml-auto small text-muted">
<FontAwesomeIcon icon={icon} /> {label}
</span>
);
}
Visibility.propTypes = {
to: PropTypes.oneOf(['private', 'all_users']),
// i18n
intl: intlShape.isRequired,
};
Visibility.defaultProps = {
to: 'private',
};
function VisibilitySelect({ intl, className, ...props }) {
const { value } = props;
const icon = value === 'private' ? faEyeSlash : faEye;
return (
<span className={className}>
<span className="d-inline-block ml-1 mr-2" style={{ width: '1.5rem' }}>
<FontAwesomeIcon icon={icon} />
</span>
<select className="d-inline-block w-auto form-control" {...props}>
<option key="private" value="private">
{intl.formatMessage(messages['profile.visibility.who.just.me'])}
</option>
<option key="all_users" value="all_users">
{intl.formatMessage(messages['profile.visibility.who.everyone'])}
</option>
</select>
</span>
);
}
VisibilitySelect.propTypes = {
id: PropTypes.string,
className: PropTypes.string,
name: PropTypes.string,
value: PropTypes.oneOf(['private', 'all_users']),
onChange: PropTypes.func,
// i18n
intl: intlShape.isRequired,
};
VisibilitySelect.defaultProps = {
id: null,
className: null,
name: 'visibility',
value: null,
onChange: null,
};
const intlVisibility = injectIntl(Visibility);
const intlVisibilitySelect = injectIntl(VisibilitySelect);
export {
intlVisibility as Visibility,
intlVisibilitySelect as VisibilitySelect,
};

View File

@@ -1,16 +0,0 @@
import { defineMessages } from 'react-intl';
const messages = defineMessages({
'profile.visibility.who.just.me': {
id: 'profile.visibility.who.just.me',
defaultMessage: 'Just me',
description: 'What users can see this area?',
},
'profile.visibility.who.everyone': {
id: 'profile.visibility.who.everyone',
defaultMessage: 'Everyone on edX',
description: 'What users can see this area?',
},
});
export default messages;

View File

@@ -1,42 +0,0 @@
module.exports = {
authentication: {
userId: 9,
username: 'staff'
},
userAccount: {
loading: false,
error: null,
username: null,
email: null,
bio: null,
name: null,
country: null,
socialLinks: null,
profileImage: {
imageUrlMedium: null,
imageUrlLarge: null
},
levelOfEducation: null
},
profilePage: {
errors: {},
saveState: null,
savePhotoState: null,
currentlyEditingField: null,
account: {
socialLinks: []
},
preferences: {},
courseCertificates: [],
drafts: {},
isLoadingProfile: true
},
router: {
location: {
pathname: '/u/verified',
search: '',
hash: ''
},
action: 'POP'
}
};

View File

@@ -1,138 +0,0 @@
module.exports = {
authentication: {
userId: 9,
username: 'staff'
},
userAccount: {
loading: false,
error: null,
username: 'staff',
email: 'staff@example.com',
bio: 'This is my bio',
name: 'Lemon Seltzer',
country: 'ME',
socialLinks: [
{
platform: 'facebook',
socialLink: 'https://www.facebook.com/aloha'
},
{
platform: 'twitter',
socialLink: 'https://www.twitter.com/ALOHA'
}
],
profileImage: {
imageUrlFull: 'http://localhost:18000/media/profile-images/d2a9bdc2ba165dcefc73265c54bf9a20_500.jpg?v=1552495012',
imageUrlLarge: 'http://localhost:18000/media/profile-images/d2a9bdc2ba165dcefc73265c54bf9a20_120.jpg?v=1552495012',
imageUrlMedium: 'http://localhost:18000/media/profile-images/d2a9bdc2ba165dcefc73265c54bf9a20_50.jpg?v=1552495012',
imageUrlSmall: 'http://localhost:18000/media/profile-images/d2a9bdc2ba165dcefc73265c54bf9a20_30.jpg?v=1552495012',
hasImage: true
},
levelOfEducation: 'el',
mailingAddress: null,
extendedProfile: [],
dateJoined: '2017-06-07T00:44:23Z',
accomplishmentsShared: false,
isActive: true,
yearOfBirth: 1901,
goals: null,
languageProficiencies: [
{
code: 'yo'
}
],
courseCertificates: null,
requiresParentalConsent: false,
secondaryEmail: null,
timeZone: null,
gender: null,
accountPrivacy: 'custom'
},
profilePage: {
errors: {},
saveState: 'pending',
savePhotoState: null,
currentlyEditingField: 'bio',
account: {
mailingAddress: null,
profileImage: {
imageUrlFull: 'http://localhost:18000/media/profile-images/d2a9bdc2ba165dcefc73265c54bf9a20_500.jpg?v=1552495012',
imageUrlLarge: 'http://localhost:18000/media/profile-images/d2a9bdc2ba165dcefc73265c54bf9a20_120.jpg?v=1552495012',
imageUrlMedium: 'http://localhost:18000/media/profile-images/d2a9bdc2ba165dcefc73265c54bf9a20_50.jpg?v=1552495012',
imageUrlSmall: 'http://localhost:18000/media/profile-images/d2a9bdc2ba165dcefc73265c54bf9a20_30.jpg?v=1552495012',
hasImage: true
},
extendedProfile: [],
dateJoined: '2017-06-07T00:44:23Z',
accomplishmentsShared: false,
email: 'staff@example.com',
username: 'staff',
bio: 'This is my bio',
isActive: true,
yearOfBirth: 1901,
goals: null,
languageProficiencies: [
{
code: 'yo'
}
],
courseCertificates: null,
requiresParentalConsent: false,
name: 'Lemon Seltzer',
secondaryEmail: null,
country: 'ME',
socialLinks: [
{
platform: 'facebook',
socialLink: 'https://www.facebook.com/aloha'
},
{
platform: 'twitter',
socialLink: 'https://www.twitter.com/ALOHA'
}
],
timeZone: null,
levelOfEducation: 'el',
gender: null,
accountPrivacy: 'custom'
},
preferences: {
visibilityUserLocation: 'all_users',
visibilitySocialLinks: 'all_users',
visibilityCertificates: 'private',
visibilityLevelOfEducation: 'private',
visibilityCourseCertificates: 'all_users',
prefLang: 'en',
visibilityBio: 'all_users',
visibilityName: 'private',
visibilityLanguageProficiencies: 'all_users',
visibilityCountry: 'all_users',
accountPrivacy: 'custom'
},
courseCertificates: [
{
username: 'staff',
status: 'downloadable',
courseDisplayName: 'edX Demonstration Course',
grade: '0.89',
courseId: 'course-v1:edX+DemoX+Demo_Course',
courseOrganization: 'edX',
modifiedDate: '2019-03-04T19:31:39.930255Z',
isPassing: true,
downloadUrl: 'http://www.example.com/',
certificateType: 'verified',
createdDate: '2019-03-04T19:31:39.896806Z'
}
],
drafts: {},
isLoadingProfile: false
},
router: {
location: {
pathname: '/u/staff',
search: '',
hash: ''
},
action: 'POP'
}
};

View File

@@ -1,99 +0,0 @@
module.exports = {
authentication: {
userId: 9,
username: 'staff'
},
userAccount: {
loading: false,
error: null,
username: 'staff',
email: 'staff@example.com',
bio: 'This is my bio',
name: 'Lemon Seltzer',
country: 'ME',
socialLinks: [
{
platform: 'facebook',
socialLink: 'https://www.facebook.com/aloha'
},
{
platform: 'twitter',
socialLink: 'https://www.twitter.com/ALOHA'
}
],
profileImage: {
imageUrlFull: 'http://localhost:18000/media/profile-images/d2a9bdc2ba165dcefc73265c54bf9a20_500.jpg?v=1552495012',
imageUrlLarge: 'http://localhost:18000/media/profile-images/d2a9bdc2ba165dcefc73265c54bf9a20_120.jpg?v=1552495012',
imageUrlMedium: 'http://localhost:18000/media/profile-images/d2a9bdc2ba165dcefc73265c54bf9a20_50.jpg?v=1552495012',
imageUrlSmall: 'http://localhost:18000/media/profile-images/d2a9bdc2ba165dcefc73265c54bf9a20_30.jpg?v=1552495012',
hasImage: true
},
levelOfEducation: 'el',
mailingAddress: null,
extendedProfile: [],
dateJoined: '2017-06-07T00:44:23Z',
accomplishmentsShared: false,
isActive: true,
yearOfBirth: 1901,
goals: null,
languageProficiencies: [
{
code: 'yo'
}
],
courseCertificates: null,
requiresParentalConsent: false,
secondaryEmail: null,
timeZone: null,
gender: null,
accountPrivacy: 'custom'
},
profilePage: {
errors: {},
saveState: null,
savePhotoState: null,
currentlyEditingField: null,
account: {
mailingAddress: null,
profileImage: {
imageUrlFull: 'http://localhost:18000/static/images/profiles/default_500.png',
imageUrlLarge: 'http://localhost:18000/static/images/profiles/default_120.png',
imageUrlMedium: 'http://localhost:18000/static/images/profiles/default_50.png',
imageUrlSmall: 'http://localhost:18000/static/images/profiles/default_30.png',
hasImage: false
},
extendedProfile: [],
dateJoined: '2017-06-07T00:44:19Z',
accomplishmentsShared: false,
email: 'verified@example.com',
username: 'verified',
bio: null,
isActive: true,
yearOfBirth: null,
goals: null,
languageProficiencies: [],
courseCertificates: null,
requiresParentalConsent: true,
name: '',
secondaryEmail: null,
country: null,
socialLinks: [],
timeZone: null,
levelOfEducation: null,
gender: null,
accountPrivacy: 'private'
},
preferences: {},
courseCertificates: [],
drafts: {},
isLoadingProfile: false
},
router: {
location: {
pathname: '/u/verified',
search: '',
hash: ''
},
action: 'POP'
}
};

View File

@@ -1,138 +0,0 @@
module.exports = {
authentication: {
userId: 9,
username: 'staff'
},
userAccount: {
loading: false,
error: null,
username: 'staff',
email: 'staff@example.com',
bio: 'This is my bio',
name: 'Lemon Seltzer',
country: 'ME',
socialLinks: [
{
platform: 'facebook',
socialLink: 'https://www.facebook.com/aloha'
},
{
platform: 'twitter',
socialLink: 'https://www.twitter.com/ALOHA'
}
],
profileImage: {
imageUrlFull: 'http://localhost:18000/media/profile-images/d2a9bdc2ba165dcefc73265c54bf9a20_500.jpg?v=1552495012',
imageUrlLarge: 'http://localhost:18000/media/profile-images/d2a9bdc2ba165dcefc73265c54bf9a20_120.jpg?v=1552495012',
imageUrlMedium: 'http://localhost:18000/media/profile-images/d2a9bdc2ba165dcefc73265c54bf9a20_50.jpg?v=1552495012',
imageUrlSmall: 'http://localhost:18000/media/profile-images/d2a9bdc2ba165dcefc73265c54bf9a20_30.jpg?v=1552495012',
hasImage: true
},
levelOfEducation: 'el',
mailingAddress: null,
extendedProfile: [],
dateJoined: '2017-06-07T00:44:23Z',
accomplishmentsShared: false,
isActive: true,
yearOfBirth: 1901,
goals: null,
languageProficiencies: [
{
code: 'yo'
}
],
courseCertificates: null,
requiresParentalConsent: false,
secondaryEmail: null,
timeZone: null,
gender: null,
accountPrivacy: 'custom'
},
profilePage: {
errors: {},
saveState: null,
savePhotoState: null,
currentlyEditingField: null,
account: {
mailingAddress: null,
profileImage: {
imageUrlFull: 'http://localhost:18000/media/profile-images/d2a9bdc2ba165dcefc73265c54bf9a20_500.jpg?v=1552495012',
imageUrlLarge: 'http://localhost:18000/media/profile-images/d2a9bdc2ba165dcefc73265c54bf9a20_120.jpg?v=1552495012',
imageUrlMedium: 'http://localhost:18000/media/profile-images/d2a9bdc2ba165dcefc73265c54bf9a20_50.jpg?v=1552495012',
imageUrlSmall: 'http://localhost:18000/media/profile-images/d2a9bdc2ba165dcefc73265c54bf9a20_30.jpg?v=1552495012',
hasImage: true
},
extendedProfile: [],
dateJoined: '2017-06-07T00:44:23Z',
accomplishmentsShared: false,
email: 'staff@example.com',
username: 'staff',
bio: 'This is my bio',
isActive: true,
yearOfBirth: 1901,
goals: null,
languageProficiencies: [
{
code: 'yo'
}
],
courseCertificates: null,
requiresParentalConsent: false,
name: 'Lemon Seltzer',
secondaryEmail: null,
country: 'ME',
socialLinks: [
{
platform: 'facebook',
socialLink: 'https://www.facebook.com/aloha'
},
{
platform: 'twitter',
socialLink: 'https://www.twitter.com/ALOHA'
}
],
timeZone: null,
levelOfEducation: 'el',
gender: null,
accountPrivacy: 'custom'
},
preferences: {
visibilityUserLocation: 'all_users',
visibilitySocialLinks: 'all_users',
visibilityCertificates: 'private',
visibilityLevelOfEducation: 'private',
visibilityCourseCertificates: 'all_users',
prefLang: 'en',
visibilityBio: 'all_users',
visibilityName: 'private',
visibilityLanguageProficiencies: 'all_users',
visibilityCountry: 'all_users',
accountPrivacy: 'custom'
},
courseCertificates: [
{
username: 'staff',
status: 'downloadable',
courseDisplayName: 'edX Demonstration Course',
grade: '0.89',
courseId: 'course-v1:edX+DemoX+Demo_Course',
courseOrganization: 'edX',
modifiedDate: '2019-03-04T19:31:39.930255Z',
isPassing: true,
downloadUrl: 'http://www.example.com/',
certificateType: 'verified',
createdDate: '2019-03-04T19:31:39.896806Z'
}
],
drafts: {},
isLoadingProfile: false
},
router: {
location: {
pathname: '/u/staff',
search: '',
hash: ''
},
action: 'POP'
}
};

View File

@@ -1,2276 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<ProfilePage /> Renders correctly in various states app loading 1`] = `
<div>
<div
className="profile-page-bg-banner bg-primary d-none d-md-block p-relative"
/>
<div
className="d-flex justify-content-center align-items-center flex-column"
style={
Object {
"height": "50vh",
}
}
>
<div
className="spinner-border text-primary"
role="status"
/>
</div>
</div>
`;
exports[`<ProfilePage /> Renders correctly in various states viewing other profile 1`] = `
<div
className="profile-page"
>
<div
className="profile-page-bg-banner bg-primary d-none d-md-block p-relative"
/>
<div
className="container-fluid"
>
<div
className="row align-items-center pt-4 mb-4 pt-md-0 mb-md-0"
>
<div
className="col-auto col-md-4 col-lg-3"
>
<div
className="d-flex align-items-center d-md-block"
>
<div
className="profile-avatar-wrap position-relative"
>
<div
className="profile-avatar rounded-circle bg-light"
>
<IconMock
aria-hidden={true}
className="text-muted"
focusable="false"
role="img"
viewBox="0 0 24 24"
/>
</div>
<form
encType="multipart/form-data"
onSubmit={[Function]}
>
<input
accept=".jpg, .jpeg, .png"
className="d-none form-control-file"
id="photo-file"
name="file"
onChange={[Function]}
type="file"
/>
</form>
</div>
</div>
</div>
<div
className="col pl-0"
>
<div
className="d-md-none"
>
<h1
className="h2 mb-0 font-weight-bold"
>
verified
</h1>
<p
className="mb-0"
>
<span>
Member since
<span>
2017
</span>
</span>
</p>
<hr
className="d-none d-md-block"
/>
</div>
<div
className="d-none d-md-block float-right"
/>
</div>
</div>
<div
className="row"
>
<div
className="col-md-4 col-lg-4"
>
<div
className="d-none d-md-block mb-4"
>
<h1
className="h2 mb-0 font-weight-bold"
>
verified
</h1>
<p
className="mb-0"
>
<span>
Member since
<span>
2017
</span>
</span>
</p>
<hr
className="d-none d-md-block"
/>
</div>
<div
className="d-md-none mb-4"
/>
<div
className="pgn-transition-replace-group position-relative mb-5"
style={
Object {
"height": null,
}
}
/>
<div
className="pgn-transition-replace-group position-relative mb-5"
style={
Object {
"height": null,
}
}
/>
<div
className="pgn-transition-replace-group position-relative mb-5"
style={
Object {
"height": null,
}
}
/>
<div
className="pgn-transition-replace-group position-relative mb-5"
style={
Object {
"height": null,
}
}
/>
<div
className="pgn-transition-replace-group position-relative mb-5"
style={
Object {
"height": null,
}
}
/>
</div>
<div
className="pt-md-3 col-md-8 col-lg-7 offset-lg-1"
>
<div
className="pgn-transition-replace-group position-relative mb-5"
style={
Object {
"height": null,
}
}
/>
<div
className="pgn-transition-replace-group position-relative mb-4"
style={
Object {
"height": null,
}
}
/>
</div>
</div>
</div>
</div>
`;
exports[`<ProfilePage /> Renders correctly in various states viewing own profile 1`] = `
<div
className="profile-page"
>
<div
className="profile-page-bg-banner bg-primary d-none d-md-block p-relative"
/>
<div
className="container-fluid"
>
<div
className="row align-items-center pt-4 mb-4 pt-md-0 mb-md-0"
>
<div
className="col-auto col-md-4 col-lg-3"
>
<div
className="d-flex align-items-center d-md-block"
>
<div
className="profile-avatar-wrap position-relative"
>
<div
className="profile-avatar rounded-circle bg-light"
>
<div
className="profile-avatar-menu-container"
>
<div
className="dropdown"
>
<button
aria-expanded={false}
aria-haspopup="true"
className="btn dropdown-toggle btn-primary"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
<span>
Change
</span>
</button>
<div
aria-hidden={true}
aria-label={
<FormattedMessage
defaultMessage="Change"
description="Change photo button"
id="profile.profileavatar.change-button"
values={Object {}}
/>
}
className="dropdown-menu"
role="menu"
>
<button
className="dropdown-item"
onClick={[Function]}
onKeyDown={[Function]}
>
<span>
Upload Photo
</span>
</button>
<button
className="dropdown-item"
onClick={[Function]}
onKeyDown={[Function]}
>
<span>
Remove
</span>
</button>
</div>
</div>
</div>
<img
alt="profile avatar"
className="w-100 h-100 d-block rounded-circle overflow-hidden"
src="http://localhost:18000/media/profile-images/d2a9bdc2ba165dcefc73265c54bf9a20_500.jpg?v=1552495012"
style={
Object {
"objectFit": "cover",
}
}
/>
</div>
<form
encType="multipart/form-data"
onSubmit={[Function]}
>
<input
accept=".jpg, .jpeg, .png"
className="d-none form-control-file"
id="photo-file"
name="file"
onChange={[Function]}
type="file"
/>
</form>
</div>
</div>
</div>
<div
className="col pl-0"
>
<div
className="d-md-none"
>
<h1
className="h2 mb-0 font-weight-bold"
>
staff
</h1>
<p
className="mb-0"
>
<span>
Member since
<span>
2017
</span>
</span>
</p>
<hr
className="d-none d-md-block"
/>
</div>
<div
className="d-none d-md-block float-right"
>
<a
className="btn btn-primary"
href="undefined/records"
onClick={[Function]}
rel="noopener"
target="_blank"
>
View My Records
<span>
<span
aria-hidden={false}
aria-label="Opens in a new window"
className="fa fa-external-link"
title="Opens in a new window"
/>
</span>
</a>
</div>
</div>
</div>
<div
className="row"
>
<div
className="col-md-4 col-lg-4"
>
<div
className="d-none d-md-block mb-4"
>
<h1
className="h2 mb-0 font-weight-bold"
>
staff
</h1>
<p
className="mb-0"
>
<span>
Member since
<span>
2017
</span>
</span>
</p>
<hr
className="d-none d-md-block"
/>
</div>
<div
className="d-md-none mb-4"
>
<a
className="btn btn-primary"
href="undefined/records"
onClick={[Function]}
rel="noopener"
target="_blank"
>
View My Records
<span>
<span
aria-hidden={false}
aria-label="Opens in a new window"
className="fa fa-external-link"
title="Opens in a new window"
/>
</span>
</a>
</div>
<div
className="pgn-transition-replace-group position-relative mb-5"
style={
Object {
"height": null,
}
}
>
<div
style={
Object {
"padding": ".1px 0",
}
}
>
<div
className="editable-item-header mb-2"
>
<h2
className="edit-section-header"
id={null}
>
Full Name
<button
className="btn btn-sm btn-link float-right px-0"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
style={
Object {
"marginTop": "-.35rem",
}
}
type="button"
>
<svg
aria-hidden="true"
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
data-icon="pencil-alt"
data-prefix="fas"
focusable="false"
role="img"
style={Object {}}
viewBox="0 0 512 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
fill="currentColor"
style={Object {}}
/>
</svg>
Edit
</button>
</h2>
<p
className="mb-0"
>
<span
className="ml-auto small text-muted"
>
<svg
aria-hidden="true"
className="svg-inline--fa fa-eye-slash fa-w-20 "
data-icon="eye-slash"
data-prefix="far"
focusable="false"
role="img"
style={Object {}}
viewBox="0 0 640 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M634 471L36 3.51A16 16 0 0 0 13.51 6l-10 12.49A16 16 0 0 0 6 41l598 467.49a16 16 0 0 0 22.49-2.49l10-12.49A16 16 0 0 0 634 471zM296.79 146.47l134.79 105.38C429.36 191.91 380.48 144 320 144a112.26 112.26 0 0 0-23.21 2.47zm46.42 219.07L208.42 260.16C210.65 320.09 259.53 368 320 368a113 113 0 0 0 23.21-2.46zM320 112c98.65 0 189.09 55 237.93 144a285.53 285.53 0 0 1-44 60.2l37.74 29.5a333.7 333.7 0 0 0 52.9-75.11 32.35 32.35 0 0 0 0-29.19C550.29 135.59 442.93 64 320 64c-36.7 0-71.71 7-104.63 18.81l46.41 36.29c18.94-4.3 38.34-7.1 58.22-7.1zm0 288c-98.65 0-189.08-55-237.93-144a285.47 285.47 0 0 1 44.05-60.19l-37.74-29.5a333.6 333.6 0 0 0-52.89 75.1 32.35 32.35 0 0 0 0 29.19C89.72 376.41 197.08 448 320 448c36.7 0 71.71-7.05 104.63-18.81l-46.41-36.28C359.28 397.2 339.89 400 320 400z"
fill="currentColor"
style={Object {}}
/>
</svg>
Just me
</span>
</p>
</div>
<p
className="h5"
>
Lemon Seltzer
</p>
<small
className="form-text text-muted"
>
This is the name that appears in your account and on your certificates.
</small>
</div>
</div>
<div
className="pgn-transition-replace-group position-relative mb-5"
style={
Object {
"height": null,
}
}
>
<div
style={
Object {
"padding": ".1px 0",
}
}
>
<div
className="editable-item-header mb-2"
>
<h2
className="edit-section-header"
id={null}
>
Location
<button
className="btn btn-sm btn-link float-right px-0"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
style={
Object {
"marginTop": "-.35rem",
}
}
type="button"
>
<svg
aria-hidden="true"
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
data-icon="pencil-alt"
data-prefix="fas"
focusable="false"
role="img"
style={Object {}}
viewBox="0 0 512 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
fill="currentColor"
style={Object {}}
/>
</svg>
Edit
</button>
</h2>
<p
className="mb-0"
>
<span
className="ml-auto small text-muted"
>
<svg
aria-hidden="true"
className="svg-inline--fa fa-eye fa-w-18 "
data-icon="eye"
data-prefix="far"
focusable="false"
role="img"
style={Object {}}
viewBox="0 0 576 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M288 144a110.94 110.94 0 0 0-31.24 5 55.4 55.4 0 0 1 7.24 27 56 56 0 0 1-56 56 55.4 55.4 0 0 1-27-7.24A111.71 111.71 0 1 0 288 144zm284.52 97.4C518.29 135.59 410.93 64 288 64S57.68 135.64 3.48 241.41a32.35 32.35 0 0 0 0 29.19C57.71 376.41 165.07 448 288 448s230.32-71.64 284.52-177.41a32.35 32.35 0 0 0 0-29.19zM288 400c-98.65 0-189.09-55-237.93-144C98.91 167 189.34 112 288 112s189.09 55 237.93 144C477.1 345 386.66 400 288 400z"
fill="currentColor"
style={Object {}}
/>
</svg>
Everyone on edX
</span>
</p>
</div>
<p
className="h5"
>
Montenegro
</p>
</div>
</div>
<div
className="pgn-transition-replace-group position-relative mb-5"
style={
Object {
"height": null,
}
}
>
<div
style={
Object {
"padding": ".1px 0",
}
}
>
<div
className="editable-item-header mb-2"
>
<h2
className="edit-section-header"
id={null}
>
Primary Language Spoken
<button
className="btn btn-sm btn-link float-right px-0"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
style={
Object {
"marginTop": "-.35rem",
}
}
type="button"
>
<svg
aria-hidden="true"
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
data-icon="pencil-alt"
data-prefix="fas"
focusable="false"
role="img"
style={Object {}}
viewBox="0 0 512 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
fill="currentColor"
style={Object {}}
/>
</svg>
Edit
</button>
</h2>
<p
className="mb-0"
>
<span
className="ml-auto small text-muted"
>
<svg
aria-hidden="true"
className="svg-inline--fa fa-eye fa-w-18 "
data-icon="eye"
data-prefix="far"
focusable="false"
role="img"
style={Object {}}
viewBox="0 0 576 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M288 144a110.94 110.94 0 0 0-31.24 5 55.4 55.4 0 0 1 7.24 27 56 56 0 0 1-56 56 55.4 55.4 0 0 1-27-7.24A111.71 111.71 0 1 0 288 144zm284.52 97.4C518.29 135.59 410.93 64 288 64S57.68 135.64 3.48 241.41a32.35 32.35 0 0 0 0 29.19C57.71 376.41 165.07 448 288 448s230.32-71.64 284.52-177.41a32.35 32.35 0 0 0 0-29.19zM288 400c-98.65 0-189.09-55-237.93-144C98.91 167 189.34 112 288 112s189.09 55 237.93 144C477.1 345 386.66 400 288 400z"
fill="currentColor"
style={Object {}}
/>
</svg>
Everyone on edX
</span>
</p>
</div>
<p
className="h5"
>
Yoruba
</p>
</div>
</div>
<div
className="pgn-transition-replace-group position-relative mb-5"
style={
Object {
"height": null,
}
}
>
<div
style={
Object {
"padding": ".1px 0",
}
}
>
<div
className="editable-item-header mb-2"
>
<h2
className="edit-section-header"
id={null}
>
Education
<button
className="btn btn-sm btn-link float-right px-0"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
style={
Object {
"marginTop": "-.35rem",
}
}
type="button"
>
<svg
aria-hidden="true"
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
data-icon="pencil-alt"
data-prefix="fas"
focusable="false"
role="img"
style={Object {}}
viewBox="0 0 512 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
fill="currentColor"
style={Object {}}
/>
</svg>
Edit
</button>
</h2>
<p
className="mb-0"
>
<span
className="ml-auto small text-muted"
>
<svg
aria-hidden="true"
className="svg-inline--fa fa-eye-slash fa-w-20 "
data-icon="eye-slash"
data-prefix="far"
focusable="false"
role="img"
style={Object {}}
viewBox="0 0 640 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M634 471L36 3.51A16 16 0 0 0 13.51 6l-10 12.49A16 16 0 0 0 6 41l598 467.49a16 16 0 0 0 22.49-2.49l10-12.49A16 16 0 0 0 634 471zM296.79 146.47l134.79 105.38C429.36 191.91 380.48 144 320 144a112.26 112.26 0 0 0-23.21 2.47zm46.42 219.07L208.42 260.16C210.65 320.09 259.53 368 320 368a113 113 0 0 0 23.21-2.46zM320 112c98.65 0 189.09 55 237.93 144a285.53 285.53 0 0 1-44 60.2l37.74 29.5a333.7 333.7 0 0 0 52.9-75.11 32.35 32.35 0 0 0 0-29.19C550.29 135.59 442.93 64 320 64c-36.7 0-71.71 7-104.63 18.81l46.41 36.29c18.94-4.3 38.34-7.1 58.22-7.1zm0 288c-98.65 0-189.08-55-237.93-144a285.47 285.47 0 0 1 44.05-60.19l-37.74-29.5a333.6 333.6 0 0 0-52.89 75.1 32.35 32.35 0 0 0 0 29.19C89.72 376.41 197.08 448 320 448c36.7 0 71.71-7.05 104.63-18.81l-46.41-36.28C359.28 397.2 339.89 400 320 400z"
fill="currentColor"
style={Object {}}
/>
</svg>
Just me
</span>
</p>
</div>
<p
className="h5"
>
Elementary/primary school
</p>
</div>
</div>
<div
className="pgn-transition-replace-group position-relative mb-5"
style={
Object {
"height": null,
}
}
>
<div
style={
Object {
"padding": ".1px 0",
}
}
>
<div
className="editable-item-header mb-2"
>
<h2
className="edit-section-header"
id={null}
>
Social Links
<button
className="btn btn-sm btn-link float-right px-0"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
style={
Object {
"marginTop": "-.35rem",
}
}
type="button"
>
<svg
aria-hidden="true"
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
data-icon="pencil-alt"
data-prefix="fas"
focusable="false"
role="img"
style={Object {}}
viewBox="0 0 512 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
fill="currentColor"
style={Object {}}
/>
</svg>
Edit
</button>
</h2>
<p
className="mb-0"
>
<span
className="ml-auto small text-muted"
>
<svg
aria-hidden="true"
className="svg-inline--fa fa-eye fa-w-18 "
data-icon="eye"
data-prefix="far"
focusable="false"
role="img"
style={Object {}}
viewBox="0 0 576 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M288 144a110.94 110.94 0 0 0-31.24 5 55.4 55.4 0 0 1 7.24 27 56 56 0 0 1-56 56 55.4 55.4 0 0 1-27-7.24A111.71 111.71 0 1 0 288 144zm284.52 97.4C518.29 135.59 410.93 64 288 64S57.68 135.64 3.48 241.41a32.35 32.35 0 0 0 0 29.19C57.71 376.41 165.07 448 288 448s230.32-71.64 284.52-177.41a32.35 32.35 0 0 0 0-29.19zM288 400c-98.65 0-189.09-55-237.93-144C98.91 167 189.34 112 288 112s189.09 55 237.93 144C477.1 345 386.66 400 288 400z"
fill="currentColor"
style={Object {}}
/>
</svg>
Everyone on edX
</span>
</p>
</div>
<ul
className="list-unstyled"
>
<li
className="form-group"
>
<a
className="font-weight-bold"
href="https://www.twitter.com/ALOHA"
>
<svg
aria-hidden="true"
className="svg-inline--fa fa-twitter fa-w-16 mr-2"
data-icon="twitter"
data-prefix="fab"
focusable="false"
role="img"
style={Object {}}
viewBox="0 0 512 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M459.37 151.716c.325 4.548.325 9.097.325 13.645 0 138.72-105.583 298.558-298.558 298.558-59.452 0-114.68-17.219-161.137-47.106 8.447.974 16.568 1.299 25.34 1.299 49.055 0 94.213-16.568 130.274-44.832-46.132-.975-84.792-31.188-98.112-72.772 6.498.974 12.995 1.624 19.818 1.624 9.421 0 18.843-1.3 27.614-3.573-48.081-9.747-84.143-51.98-84.143-102.985v-1.299c13.969 7.797 30.214 12.67 47.431 13.319-28.264-18.843-46.781-51.005-46.781-87.391 0-19.492 5.197-37.36 14.294-52.954 51.655 63.675 129.3 105.258 216.365 109.807-1.624-7.797-2.599-15.918-2.599-24.04 0-57.828 46.782-104.934 104.934-104.934 30.213 0 57.502 12.67 76.67 33.137 23.715-4.548 46.456-13.32 66.599-25.34-7.798 24.366-24.366 44.833-46.132 57.827 21.117-2.273 41.584-8.122 60.426-16.243-14.292 20.791-32.161 39.308-52.628 54.253z"
fill="currentColor"
style={Object {}}
/>
</svg>
Twitter
</a>
</li>
<li
className="form-group"
>
<a
className="font-weight-bold"
href="https://www.facebook.com/aloha"
>
<svg
aria-hidden="true"
className="svg-inline--fa fa-facebook fa-w-14 mr-2"
data-icon="facebook"
data-prefix="fab"
focusable="false"
role="img"
style={Object {}}
viewBox="0 0 448 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M448 56.7v398.5c0 13.7-11.1 24.7-24.7 24.7H309.1V306.5h58.2l8.7-67.6h-67v-43.2c0-19.6 5.4-32.9 33.5-32.9h35.8v-60.5c-6.2-.8-27.4-2.7-52.2-2.7-51.6 0-87 31.5-87 89.4v49.9h-58.4v67.6h58.4V480H24.7C11.1 480 0 468.9 0 455.3V56.7C0 43.1 11.1 32 24.7 32h398.5c13.7 0 24.8 11.1 24.8 24.7z"
fill="currentColor"
style={Object {}}
/>
</svg>
Facebook
</a>
</li>
<li
className="form-group"
>
<div>
<button
className="pl-0 text-left btn btn-link"
onClick={[Function]}
onKeyDown={[Function]}
tabIndex={0}
>
<svg
aria-hidden="true"
className="svg-inline--fa fa-plus fa-w-14 fa-xs mr-2"
data-icon="plus"
data-prefix="fas"
focusable="false"
role="img"
style={Object {}}
viewBox="0 0 448 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M416 208H272V64c0-17.67-14.33-32-32-32h-32c-17.67 0-32 14.33-32 32v144H32c-17.67 0-32 14.33-32 32v32c0 17.67 14.33 32 32 32h144v144c0 17.67 14.33 32 32 32h32c17.67 0 32-14.33 32-32V304h144c17.67 0 32-14.33 32-32v-32c0-17.67-14.33-32-32-32z"
fill="currentColor"
style={Object {}}
/>
</svg>
Add
LinkedIn
</button>
</div>
</li>
</ul>
</div>
</div>
</div>
<div
className="pt-md-3 col-md-8 col-lg-7 offset-lg-1"
>
<div
className="pgn-transition-replace-group position-relative mb-5"
style={
Object {
"height": null,
}
}
>
<div
style={
Object {
"padding": ".1px 0",
}
}
>
<div
className="editable-item-header mb-2"
>
<h2
className="edit-section-header"
id={null}
>
About Me
<button
className="btn btn-sm btn-link float-right px-0"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
style={
Object {
"marginTop": "-.35rem",
}
}
type="button"
>
<svg
aria-hidden="true"
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
data-icon="pencil-alt"
data-prefix="fas"
focusable="false"
role="img"
style={Object {}}
viewBox="0 0 512 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
fill="currentColor"
style={Object {}}
/>
</svg>
Edit
</button>
</h2>
<p
className="mb-0"
>
<span
className="ml-auto small text-muted"
>
<svg
aria-hidden="true"
className="svg-inline--fa fa-eye fa-w-18 "
data-icon="eye"
data-prefix="far"
focusable="false"
role="img"
style={Object {}}
viewBox="0 0 576 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M288 144a110.94 110.94 0 0 0-31.24 5 55.4 55.4 0 0 1 7.24 27 56 56 0 0 1-56 56 55.4 55.4 0 0 1-27-7.24A111.71 111.71 0 1 0 288 144zm284.52 97.4C518.29 135.59 410.93 64 288 64S57.68 135.64 3.48 241.41a32.35 32.35 0 0 0 0 29.19C57.71 376.41 165.07 448 288 448s230.32-71.64 284.52-177.41a32.35 32.35 0 0 0 0-29.19zM288 400c-98.65 0-189.09-55-237.93-144C98.91 167 189.34 112 288 112s189.09 55 237.93 144C477.1 345 386.66 400 288 400z"
fill="currentColor"
style={Object {}}
/>
</svg>
Everyone on edX
</span>
</p>
</div>
<p
className="lead"
>
This is my bio
</p>
</div>
</div>
<div
className="pgn-transition-replace-group position-relative mb-4"
style={
Object {
"height": null,
}
}
>
<div
style={
Object {
"padding": ".1px 0",
}
}
>
<div
className="editable-item-header mb-2"
>
<h2
className="edit-section-header"
id={null}
>
My Certificates
<button
className="btn btn-sm btn-link float-right px-0"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
style={
Object {
"marginTop": "-.35rem",
}
}
type="button"
>
<svg
aria-hidden="true"
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
data-icon="pencil-alt"
data-prefix="fas"
focusable="false"
role="img"
style={Object {}}
viewBox="0 0 512 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
fill="currentColor"
style={Object {}}
/>
</svg>
Edit
</button>
</h2>
<p
className="mb-0"
>
<span
className="ml-auto small text-muted"
>
<svg
aria-hidden="true"
className="svg-inline--fa fa-eye fa-w-18 "
data-icon="eye"
data-prefix="far"
focusable="false"
role="img"
style={Object {}}
viewBox="0 0 576 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M288 144a110.94 110.94 0 0 0-31.24 5 55.4 55.4 0 0 1 7.24 27 56 56 0 0 1-56 56 55.4 55.4 0 0 1-27-7.24A111.71 111.71 0 1 0 288 144zm284.52 97.4C518.29 135.59 410.93 64 288 64S57.68 135.64 3.48 241.41a32.35 32.35 0 0 0 0 29.19C57.71 376.41 165.07 448 288 448s230.32-71.64 284.52-177.41a32.35 32.35 0 0 0 0-29.19zM288 400c-98.65 0-189.09-55-237.93-144C98.91 167 189.34 112 288 112s189.09 55 237.93 144C477.1 345 386.66 400 288 400z"
fill="currentColor"
style={Object {}}
/>
</svg>
Everyone on edX
</span>
</p>
</div>
<div
className="row align-items-stretch"
>
<div
className="col col-sm-6 d-flex align-items-stretch"
>
<div
className="card mb-4 certificate flex-grow-1"
>
<div
className="certificate-type-illustration"
style={
Object {
"backgroundImage": "url(null)",
}
}
/>
<div
className="card-body d-flex flex-column"
>
<div
className="card-title"
>
<p
className="small mb-0"
>
Verified Certificate
</p>
<h4
className="certificate-title"
>
edX Demonstration Course
</h4>
</div>
<p
className="small mb-0"
>
<span>
From
</span>
</p>
<p
className="h6 mb-4"
>
edX
</p>
<div
className="flex-grow-1"
/>
<p
className="small mb-2"
>
<span>
Completed on
<span>
3/4/2019
</span>
</span>
</p>
<div>
<a
className="btn btn-outline-primary"
href="http://www.example.com/"
onClick={[Function]}
rel="noopener"
target="_blank"
>
View Certificate
<span>
<span
aria-hidden={false}
aria-label="Opens in a new window"
className="fa fa-external-link"
title="Opens in a new window"
/>
</span>
</a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
`;
exports[`<ProfilePage /> Renders correctly in various states while saving an edited bio 1`] = `
<div
className="profile-page"
>
<div
className="profile-page-bg-banner bg-primary d-none d-md-block p-relative"
/>
<div
className="container-fluid"
>
<div
className="row align-items-center pt-4 mb-4 pt-md-0 mb-md-0"
>
<div
className="col-auto col-md-4 col-lg-3"
>
<div
className="d-flex align-items-center d-md-block"
>
<div
className="profile-avatar-wrap position-relative"
>
<div
className="profile-avatar rounded-circle bg-light"
>
<div
className="profile-avatar-menu-container"
>
<div
className="dropdown"
>
<button
aria-expanded={false}
aria-haspopup="true"
className="btn dropdown-toggle btn-primary"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
<span>
Change
</span>
</button>
<div
aria-hidden={true}
aria-label={
<FormattedMessage
defaultMessage="Change"
description="Change photo button"
id="profile.profileavatar.change-button"
values={Object {}}
/>
}
className="dropdown-menu"
role="menu"
>
<button
className="dropdown-item"
onClick={[Function]}
onKeyDown={[Function]}
>
<span>
Upload Photo
</span>
</button>
<button
className="dropdown-item"
onClick={[Function]}
onKeyDown={[Function]}
>
<span>
Remove
</span>
</button>
</div>
</div>
</div>
<img
alt="profile avatar"
className="w-100 h-100 d-block rounded-circle overflow-hidden"
src="http://localhost:18000/media/profile-images/d2a9bdc2ba165dcefc73265c54bf9a20_500.jpg?v=1552495012"
style={
Object {
"objectFit": "cover",
}
}
/>
</div>
<form
encType="multipart/form-data"
onSubmit={[Function]}
>
<input
accept=".jpg, .jpeg, .png"
className="d-none form-control-file"
id="photo-file"
name="file"
onChange={[Function]}
type="file"
/>
</form>
</div>
</div>
</div>
<div
className="col pl-0"
>
<div
className="d-md-none"
>
<h1
className="h2 mb-0 font-weight-bold"
>
staff
</h1>
<p
className="mb-0"
>
<span>
Member since
<span>
2017
</span>
</span>
</p>
<hr
className="d-none d-md-block"
/>
</div>
<div
className="d-none d-md-block float-right"
>
<a
className="btn btn-primary"
href="undefined/records"
onClick={[Function]}
rel="noopener"
target="_blank"
>
View My Records
<span>
<span
aria-hidden={false}
aria-label="Opens in a new window"
className="fa fa-external-link"
title="Opens in a new window"
/>
</span>
</a>
</div>
</div>
</div>
<div
className="row"
>
<div
className="col-md-4 col-lg-4"
>
<div
className="d-none d-md-block mb-4"
>
<h1
className="h2 mb-0 font-weight-bold"
>
staff
</h1>
<p
className="mb-0"
>
<span>
Member since
<span>
2017
</span>
</span>
</p>
<hr
className="d-none d-md-block"
/>
</div>
<div
className="d-md-none mb-4"
>
<a
className="btn btn-primary"
href="undefined/records"
onClick={[Function]}
rel="noopener"
target="_blank"
>
View My Records
<span>
<span
aria-hidden={false}
aria-label="Opens in a new window"
className="fa fa-external-link"
title="Opens in a new window"
/>
</span>
</a>
</div>
<div
className="pgn-transition-replace-group position-relative mb-5"
style={
Object {
"height": null,
}
}
>
<div
style={
Object {
"padding": ".1px 0",
}
}
>
<div
className="editable-item-header mb-2"
>
<h2
className="edit-section-header"
id={null}
>
Full Name
<button
className="btn btn-sm btn-link float-right px-0"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
style={
Object {
"marginTop": "-.35rem",
}
}
type="button"
>
<svg
aria-hidden="true"
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
data-icon="pencil-alt"
data-prefix="fas"
focusable="false"
role="img"
style={Object {}}
viewBox="0 0 512 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
fill="currentColor"
style={Object {}}
/>
</svg>
Edit
</button>
</h2>
<p
className="mb-0"
>
<span
className="ml-auto small text-muted"
>
<svg
aria-hidden="true"
className="svg-inline--fa fa-eye-slash fa-w-20 "
data-icon="eye-slash"
data-prefix="far"
focusable="false"
role="img"
style={Object {}}
viewBox="0 0 640 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M634 471L36 3.51A16 16 0 0 0 13.51 6l-10 12.49A16 16 0 0 0 6 41l598 467.49a16 16 0 0 0 22.49-2.49l10-12.49A16 16 0 0 0 634 471zM296.79 146.47l134.79 105.38C429.36 191.91 380.48 144 320 144a112.26 112.26 0 0 0-23.21 2.47zm46.42 219.07L208.42 260.16C210.65 320.09 259.53 368 320 368a113 113 0 0 0 23.21-2.46zM320 112c98.65 0 189.09 55 237.93 144a285.53 285.53 0 0 1-44 60.2l37.74 29.5a333.7 333.7 0 0 0 52.9-75.11 32.35 32.35 0 0 0 0-29.19C550.29 135.59 442.93 64 320 64c-36.7 0-71.71 7-104.63 18.81l46.41 36.29c18.94-4.3 38.34-7.1 58.22-7.1zm0 288c-98.65 0-189.08-55-237.93-144a285.47 285.47 0 0 1 44.05-60.19l-37.74-29.5a333.6 333.6 0 0 0-52.89 75.1 32.35 32.35 0 0 0 0 29.19C89.72 376.41 197.08 448 320 448c36.7 0 71.71-7.05 104.63-18.81l-46.41-36.28C359.28 397.2 339.89 400 320 400z"
fill="currentColor"
style={Object {}}
/>
</svg>
Just me
</span>
</p>
</div>
<p
className="h5"
>
Lemon Seltzer
</p>
<small
className="form-text text-muted"
>
This is the name that appears in your account and on your certificates.
</small>
</div>
</div>
<div
className="pgn-transition-replace-group position-relative mb-5"
style={
Object {
"height": null,
}
}
>
<div
style={
Object {
"padding": ".1px 0",
}
}
>
<div
className="editable-item-header mb-2"
>
<h2
className="edit-section-header"
id={null}
>
Location
<button
className="btn btn-sm btn-link float-right px-0"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
style={
Object {
"marginTop": "-.35rem",
}
}
type="button"
>
<svg
aria-hidden="true"
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
data-icon="pencil-alt"
data-prefix="fas"
focusable="false"
role="img"
style={Object {}}
viewBox="0 0 512 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
fill="currentColor"
style={Object {}}
/>
</svg>
Edit
</button>
</h2>
<p
className="mb-0"
>
<span
className="ml-auto small text-muted"
>
<svg
aria-hidden="true"
className="svg-inline--fa fa-eye fa-w-18 "
data-icon="eye"
data-prefix="far"
focusable="false"
role="img"
style={Object {}}
viewBox="0 0 576 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M288 144a110.94 110.94 0 0 0-31.24 5 55.4 55.4 0 0 1 7.24 27 56 56 0 0 1-56 56 55.4 55.4 0 0 1-27-7.24A111.71 111.71 0 1 0 288 144zm284.52 97.4C518.29 135.59 410.93 64 288 64S57.68 135.64 3.48 241.41a32.35 32.35 0 0 0 0 29.19C57.71 376.41 165.07 448 288 448s230.32-71.64 284.52-177.41a32.35 32.35 0 0 0 0-29.19zM288 400c-98.65 0-189.09-55-237.93-144C98.91 167 189.34 112 288 112s189.09 55 237.93 144C477.1 345 386.66 400 288 400z"
fill="currentColor"
style={Object {}}
/>
</svg>
Everyone on edX
</span>
</p>
</div>
<p
className="h5"
>
Montenegro
</p>
</div>
</div>
<div
className="pgn-transition-replace-group position-relative mb-5"
style={
Object {
"height": null,
}
}
>
<div
style={
Object {
"padding": ".1px 0",
}
}
>
<div
className="editable-item-header mb-2"
>
<h2
className="edit-section-header"
id={null}
>
Primary Language Spoken
<button
className="btn btn-sm btn-link float-right px-0"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
style={
Object {
"marginTop": "-.35rem",
}
}
type="button"
>
<svg
aria-hidden="true"
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
data-icon="pencil-alt"
data-prefix="fas"
focusable="false"
role="img"
style={Object {}}
viewBox="0 0 512 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
fill="currentColor"
style={Object {}}
/>
</svg>
Edit
</button>
</h2>
<p
className="mb-0"
>
<span
className="ml-auto small text-muted"
>
<svg
aria-hidden="true"
className="svg-inline--fa fa-eye fa-w-18 "
data-icon="eye"
data-prefix="far"
focusable="false"
role="img"
style={Object {}}
viewBox="0 0 576 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M288 144a110.94 110.94 0 0 0-31.24 5 55.4 55.4 0 0 1 7.24 27 56 56 0 0 1-56 56 55.4 55.4 0 0 1-27-7.24A111.71 111.71 0 1 0 288 144zm284.52 97.4C518.29 135.59 410.93 64 288 64S57.68 135.64 3.48 241.41a32.35 32.35 0 0 0 0 29.19C57.71 376.41 165.07 448 288 448s230.32-71.64 284.52-177.41a32.35 32.35 0 0 0 0-29.19zM288 400c-98.65 0-189.09-55-237.93-144C98.91 167 189.34 112 288 112s189.09 55 237.93 144C477.1 345 386.66 400 288 400z"
fill="currentColor"
style={Object {}}
/>
</svg>
Everyone on edX
</span>
</p>
</div>
<p
className="h5"
>
Yoruba
</p>
</div>
</div>
<div
className="pgn-transition-replace-group position-relative mb-5"
style={
Object {
"height": null,
}
}
>
<div
style={
Object {
"padding": ".1px 0",
}
}
>
<div
className="editable-item-header mb-2"
>
<h2
className="edit-section-header"
id={null}
>
Education
<button
className="btn btn-sm btn-link float-right px-0"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
style={
Object {
"marginTop": "-.35rem",
}
}
type="button"
>
<svg
aria-hidden="true"
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
data-icon="pencil-alt"
data-prefix="fas"
focusable="false"
role="img"
style={Object {}}
viewBox="0 0 512 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
fill="currentColor"
style={Object {}}
/>
</svg>
Edit
</button>
</h2>
<p
className="mb-0"
>
<span
className="ml-auto small text-muted"
>
<svg
aria-hidden="true"
className="svg-inline--fa fa-eye-slash fa-w-20 "
data-icon="eye-slash"
data-prefix="far"
focusable="false"
role="img"
style={Object {}}
viewBox="0 0 640 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M634 471L36 3.51A16 16 0 0 0 13.51 6l-10 12.49A16 16 0 0 0 6 41l598 467.49a16 16 0 0 0 22.49-2.49l10-12.49A16 16 0 0 0 634 471zM296.79 146.47l134.79 105.38C429.36 191.91 380.48 144 320 144a112.26 112.26 0 0 0-23.21 2.47zm46.42 219.07L208.42 260.16C210.65 320.09 259.53 368 320 368a113 113 0 0 0 23.21-2.46zM320 112c98.65 0 189.09 55 237.93 144a285.53 285.53 0 0 1-44 60.2l37.74 29.5a333.7 333.7 0 0 0 52.9-75.11 32.35 32.35 0 0 0 0-29.19C550.29 135.59 442.93 64 320 64c-36.7 0-71.71 7-104.63 18.81l46.41 36.29c18.94-4.3 38.34-7.1 58.22-7.1zm0 288c-98.65 0-189.08-55-237.93-144a285.47 285.47 0 0 1 44.05-60.19l-37.74-29.5a333.6 333.6 0 0 0-52.89 75.1 32.35 32.35 0 0 0 0 29.19C89.72 376.41 197.08 448 320 448c36.7 0 71.71-7.05 104.63-18.81l-46.41-36.28C359.28 397.2 339.89 400 320 400z"
fill="currentColor"
style={Object {}}
/>
</svg>
Just me
</span>
</p>
</div>
<p
className="h5"
>
Elementary/primary school
</p>
</div>
</div>
<div
className="pgn-transition-replace-group position-relative mb-5"
style={
Object {
"height": null,
}
}
>
<div
style={
Object {
"padding": ".1px 0",
}
}
>
<div
className="editable-item-header mb-2"
>
<h2
className="edit-section-header"
id={null}
>
Social Links
<button
className="btn btn-sm btn-link float-right px-0"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
style={
Object {
"marginTop": "-.35rem",
}
}
type="button"
>
<svg
aria-hidden="true"
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
data-icon="pencil-alt"
data-prefix="fas"
focusable="false"
role="img"
style={Object {}}
viewBox="0 0 512 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
fill="currentColor"
style={Object {}}
/>
</svg>
Edit
</button>
</h2>
<p
className="mb-0"
>
<span
className="ml-auto small text-muted"
>
<svg
aria-hidden="true"
className="svg-inline--fa fa-eye fa-w-18 "
data-icon="eye"
data-prefix="far"
focusable="false"
role="img"
style={Object {}}
viewBox="0 0 576 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M288 144a110.94 110.94 0 0 0-31.24 5 55.4 55.4 0 0 1 7.24 27 56 56 0 0 1-56 56 55.4 55.4 0 0 1-27-7.24A111.71 111.71 0 1 0 288 144zm284.52 97.4C518.29 135.59 410.93 64 288 64S57.68 135.64 3.48 241.41a32.35 32.35 0 0 0 0 29.19C57.71 376.41 165.07 448 288 448s230.32-71.64 284.52-177.41a32.35 32.35 0 0 0 0-29.19zM288 400c-98.65 0-189.09-55-237.93-144C98.91 167 189.34 112 288 112s189.09 55 237.93 144C477.1 345 386.66 400 288 400z"
fill="currentColor"
style={Object {}}
/>
</svg>
Everyone on edX
</span>
</p>
</div>
<ul
className="list-unstyled"
>
<li
className="form-group"
>
<a
className="font-weight-bold"
href="https://www.twitter.com/ALOHA"
>
<svg
aria-hidden="true"
className="svg-inline--fa fa-twitter fa-w-16 mr-2"
data-icon="twitter"
data-prefix="fab"
focusable="false"
role="img"
style={Object {}}
viewBox="0 0 512 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M459.37 151.716c.325 4.548.325 9.097.325 13.645 0 138.72-105.583 298.558-298.558 298.558-59.452 0-114.68-17.219-161.137-47.106 8.447.974 16.568 1.299 25.34 1.299 49.055 0 94.213-16.568 130.274-44.832-46.132-.975-84.792-31.188-98.112-72.772 6.498.974 12.995 1.624 19.818 1.624 9.421 0 18.843-1.3 27.614-3.573-48.081-9.747-84.143-51.98-84.143-102.985v-1.299c13.969 7.797 30.214 12.67 47.431 13.319-28.264-18.843-46.781-51.005-46.781-87.391 0-19.492 5.197-37.36 14.294-52.954 51.655 63.675 129.3 105.258 216.365 109.807-1.624-7.797-2.599-15.918-2.599-24.04 0-57.828 46.782-104.934 104.934-104.934 30.213 0 57.502 12.67 76.67 33.137 23.715-4.548 46.456-13.32 66.599-25.34-7.798 24.366-24.366 44.833-46.132 57.827 21.117-2.273 41.584-8.122 60.426-16.243-14.292 20.791-32.161 39.308-52.628 54.253z"
fill="currentColor"
style={Object {}}
/>
</svg>
Twitter
</a>
</li>
<li
className="form-group"
>
<a
className="font-weight-bold"
href="https://www.facebook.com/aloha"
>
<svg
aria-hidden="true"
className="svg-inline--fa fa-facebook fa-w-14 mr-2"
data-icon="facebook"
data-prefix="fab"
focusable="false"
role="img"
style={Object {}}
viewBox="0 0 448 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M448 56.7v398.5c0 13.7-11.1 24.7-24.7 24.7H309.1V306.5h58.2l8.7-67.6h-67v-43.2c0-19.6 5.4-32.9 33.5-32.9h35.8v-60.5c-6.2-.8-27.4-2.7-52.2-2.7-51.6 0-87 31.5-87 89.4v49.9h-58.4v67.6h58.4V480H24.7C11.1 480 0 468.9 0 455.3V56.7C0 43.1 11.1 32 24.7 32h398.5c13.7 0 24.8 11.1 24.8 24.7z"
fill="currentColor"
style={Object {}}
/>
</svg>
Facebook
</a>
</li>
<li
className="form-group"
>
<div>
<button
className="pl-0 text-left btn btn-link"
onClick={[Function]}
onKeyDown={[Function]}
tabIndex={0}
>
<svg
aria-hidden="true"
className="svg-inline--fa fa-plus fa-w-14 fa-xs mr-2"
data-icon="plus"
data-prefix="fas"
focusable="false"
role="img"
style={Object {}}
viewBox="0 0 448 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M416 208H272V64c0-17.67-14.33-32-32-32h-32c-17.67 0-32 14.33-32 32v144H32c-17.67 0-32 14.33-32 32v32c0 17.67 14.33 32 32 32h144v144c0 17.67 14.33 32 32 32h32c17.67 0 32-14.33 32-32V304h144c17.67 0 32-14.33 32-32v-32c0-17.67-14.33-32-32-32z"
fill="currentColor"
style={Object {}}
/>
</svg>
Add
LinkedIn
</button>
</div>
</li>
</ul>
</div>
</div>
</div>
<div
className="pt-md-3 col-md-8 col-lg-7 offset-lg-1"
>
<div
className="pgn-transition-replace-group position-relative mb-5"
style={
Object {
"height": null,
}
}
>
<div
style={
Object {
"padding": ".1px 0",
}
}
>
<div
aria-labelledby="bio-label"
role="dialog"
>
<form
onSubmit={[Function]}
>
<div
className="form-group"
>
<label
className="edit-section-header"
htmlFor="bio"
>
About Me
</label>
<textarea
aria-describedby=""
className="form-control"
id="bio"
name="bio"
onChange={[Function]}
value="This is my bio"
/>
</div>
<div
className="d-flex flex-row-reverse flex-wrap justify-content-end align-items-center"
>
<div
className="form-group d-flex flex-wrap"
>
<label
className="col-form-label"
htmlFor="visibilityBio"
>
Who can see this:
</label>
<span
className="d-flex align-items-center"
>
<span
className="d-inline-block ml-1 mr-2"
style={
Object {
"width": "1.5rem",
}
}
>
<svg
aria-hidden="true"
className="svg-inline--fa fa-eye fa-w-18 "
data-icon="eye"
data-prefix="far"
focusable="false"
role="img"
style={Object {}}
viewBox="0 0 576 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M288 144a110.94 110.94 0 0 0-31.24 5 55.4 55.4 0 0 1 7.24 27 56 56 0 0 1-56 56 55.4 55.4 0 0 1-27-7.24A111.71 111.71 0 1 0 288 144zm284.52 97.4C518.29 135.59 410.93 64 288 64S57.68 135.64 3.48 241.41a32.35 32.35 0 0 0 0 29.19C57.71 376.41 165.07 448 288 448s230.32-71.64 284.52-177.41a32.35 32.35 0 0 0 0-29.19zM288 400c-98.65 0-189.09-55-237.93-144C98.91 167 189.34 112 288 112s189.09 55 237.93 144C477.1 345 386.66 400 288 400z"
fill="currentColor"
style={Object {}}
/>
</svg>
</span>
<select
className="d-inline-block w-auto form-control"
id="visibilityBio"
name="visibilityBio"
onChange={[Function]}
type="select"
value="all_users"
>
<option
value="private"
>
Just me
</option>
<option
value="all_users"
>
Everyone on edX
</option>
</select>
</span>
</div>
<div
className="form-group flex-shrink-0 mr-auto"
>
<button
aria-live="assertive"
className="btn pgn__stateful-btn pgn__stateful-btn-state-pending btn-primary"
disabled={false}
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="submit"
>
<span
className="d-flex align-items-center justify-content-center"
>
<span
className="pgn__stateful-btn-icon"
>
<span
aria-hidden={true}
className="icon fa fa-spinner fa-spin"
id="Icon1"
/>
</span>
Saving
</span>
</button>
<button
className="btn btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
Cancel
</button>
</div>
</div>
</form>
</div>
</div>
</div>
<div
className="pgn-transition-replace-group position-relative mb-4"
style={
Object {
"height": null,
}
}
>
<div
style={
Object {
"padding": ".1px 0",
}
}
>
<div
className="editable-item-header mb-2"
>
<h2
className="edit-section-header"
id={null}
>
My Certificates
<button
className="btn btn-sm btn-link float-right px-0"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
style={
Object {
"marginTop": "-.35rem",
}
}
type="button"
>
<svg
aria-hidden="true"
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
data-icon="pencil-alt"
data-prefix="fas"
focusable="false"
role="img"
style={Object {}}
viewBox="0 0 512 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
fill="currentColor"
style={Object {}}
/>
</svg>
Edit
</button>
</h2>
<p
className="mb-0"
>
<span
className="ml-auto small text-muted"
>
<svg
aria-hidden="true"
className="svg-inline--fa fa-eye fa-w-18 "
data-icon="eye"
data-prefix="far"
focusable="false"
role="img"
style={Object {}}
viewBox="0 0 576 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M288 144a110.94 110.94 0 0 0-31.24 5 55.4 55.4 0 0 1 7.24 27 56 56 0 0 1-56 56 55.4 55.4 0 0 1-27-7.24A111.71 111.71 0 1 0 288 144zm284.52 97.4C518.29 135.59 410.93 64 288 64S57.68 135.64 3.48 241.41a32.35 32.35 0 0 0 0 29.19C57.71 376.41 165.07 448 288 448s230.32-71.64 284.52-177.41a32.35 32.35 0 0 0 0-29.19zM288 400c-98.65 0-189.09-55-237.93-144C98.91 167 189.34 112 288 112s189.09 55 237.93 144C477.1 345 386.66 400 288 400z"
fill="currentColor"
style={Object {}}
/>
</svg>
Everyone on edX
</span>
</p>
</div>
<div
className="row align-items-stretch"
>
<div
className="col col-sm-6 d-flex align-items-stretch"
>
<div
className="card mb-4 certificate flex-grow-1"
>
<div
className="certificate-type-illustration"
style={
Object {
"backgroundImage": "url(null)",
}
}
/>
<div
className="card-body d-flex flex-column"
>
<div
className="card-title"
>
<p
className="small mb-0"
>
Verified Certificate
</p>
<h4
className="certificate-title"
>
edX Demonstration Course
</h4>
</div>
<p
className="small mb-0"
>
<span>
From
</span>
</p>
<p
className="h6 mb-4"
>
edX
</p>
<div
className="flex-grow-1"
/>
<p
className="small mb-2"
>
<span>
Completed on
<span>
3/4/2019
</span>
</span>
</p>
<div>
<a
className="btn btn-outline-primary"
href="http://www.example.com/"
onClick={[Function]}
rel="noopener"
target="_blank"
>
View Certificate
<span>
<span
aria-hidden={false}
aria-label="Opens in a new window"
className="fa fa-external-link"
title="Opens in a new window"
/>
</span>
</a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
`;

View File

@@ -1,7 +0,0 @@
import React from 'react';
function Banner() {
return <div className="profile-page-bg-banner bg-primary d-none d-md-block p-relative" />;
}
export default Banner;

View File

@@ -1,21 +0,0 @@
import React from 'react';
import Banner from './Banner';
function PageLoading() {
return (
<div>
<Banner />
<div
className="d-flex justify-content-center align-items-center flex-column"
style={{
height: '50vh',
}}
>
<div className="spinner-border text-primary" role="status" />
</div>
</div>
);
}
export default PageLoading;

View File

@@ -1,71 +0,0 @@
import { connect } from 'react-redux';
import SiteHeader from '@edx/frontend-component-site-header';
import { injectIntl } from '@edx/frontend-i18n'; // eslint-disable-line
import messages from './SiteHeader.messages';
import Logo from '../../assets/logo.svg';
const mapStateToProps = (state, { intl }) => ({
logo: Logo,
logoDestination: process.env.MARKETING_SITE_BASE_URL,
logoAltText: 'edX',
mainMenu: [
{
type: 'item',
href: `${process.env.MARKETING_SITE_BASE_URL}/course`,
content: intl.formatMessage(messages['siteheader.links.courses']),
},
{
type: 'item',
href: `${process.env.MARKETING_SITE_BASE_URL}/course?program=all`,
content: intl.formatMessage(messages['siteheader.links.programs']),
},
{
type: 'item',
href: `${process.env.MARKETING_SITE_BASE_URL}/schools-partners`,
content: intl.formatMessage(messages['siteheader.links.schools']),
},
],
loggedIn: true,
username: state.userAccount.username,
avatar: state.userAccount.profileImage.hasImage ?
state.userAccount.profileImage.imageUrlMedium :
null,
userMenu: [
{
type: 'item',
href: `${process.env.LMS_BASE_URL}`,
content: intl.formatMessage(messages['siteheader.user.menu.dashboard']),
},
{
type: 'item',
href: `${process.env.BASE_URL}/u/${state.userAccount.username}`,
content: intl.formatMessage(messages['siteheader.user.menu.profile']),
},
{
type: 'item',
href: `${process.env.LMS_BASE_URL}/account/settings`,
content: intl.formatMessage(messages['siteheader.user.menu.account.settings']),
},
{
type: 'item',
href: process.env.LOGOUT_URL,
content: intl.formatMessage(messages['siteheader.user.menu.logout']),
},
],
loggedOutItems: [
{
type: 'item',
href: `${process.env.LMS_BASE_URL}/login`,
content: intl.formatMessage(messages['siteheader.user.menu.login']),
},
{
type: 'item',
href: `${process.env.LMS_BASE_URL}/register`,
content: intl.formatMessage(messages['siteheader.user.menu.register']),
},
],
});
export default injectIntl(connect(mapStateToProps)(SiteHeader));