Organize the repo and export profile module properly (#262)
* Moving files to their new homes. Subsequent commit will wire everything up again. * Relink files and split stylesheet. Requires adding resolve-url-loader to fix sass import relativity. * Remove browserslist warning. * Remove the need for ProfileMain * Fix test issues - needed thunk.
This commit is contained in:
@@ -4,20 +4,27 @@ import { App, AppProvider, APP_ERROR, APP_READY, ErrorPage } from '@edx/frontend
|
||||
import { NewRelicLoggingService } from '@edx/frontend-logging';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { Route, Switch } from 'react-router-dom';
|
||||
|
||||
import Header, { messages as headerMessages } from '@edx/frontend-component-header';
|
||||
import Footer from '../footer/Footer';
|
||||
|
||||
import appMessages from './i18n';
|
||||
import './index.scss';
|
||||
import ProfileMain from './profile/components/ProfileMain';
|
||||
import { ProfilePage, NotFoundPage } from './profile';
|
||||
import configureStore from './store';
|
||||
|
||||
App.subscribe(APP_READY, () => {
|
||||
ReactDOM.render(
|
||||
<AppProvider store={configureStore()}>
|
||||
<Header />
|
||||
<ProfileMain />
|
||||
<main>
|
||||
<Switch>
|
||||
<Route path="/u/:username" component={ProfilePage} />
|
||||
<Route path="/notfound" component={NotFoundPage} />
|
||||
<Route path="*" component={NotFoundPage} />
|
||||
</Switch>
|
||||
</main>
|
||||
<Footer />
|
||||
</AppProvider>,
|
||||
document.getElementById('root'),
|
||||
@@ -29,4 +36,3 @@ App.subscribe(APP_ERROR, (error) => {
|
||||
});
|
||||
|
||||
App.initialize({ messages: [appMessages, headerMessages], loggingService: NewRelicLoggingService });
|
||||
|
||||
|
||||
140
src/index.scss
140
src/index.scss
@@ -1,145 +1,7 @@
|
||||
@import '~@edx/paragon/scss/edx/theme.scss';
|
||||
@import '~@edx/paragon/scss/edx/fonts.scss'; // Roboto
|
||||
|
||||
$fa-font-path: "~font-awesome/fonts";
|
||||
@import "~font-awesome/scss/font-awesome";
|
||||
@import './profile/index.scss';
|
||||
|
||||
@import "~@edx/frontend-component-header/src/index";
|
||||
@import "~@edx/frontend-component-footer/src/lib/scss/site-footer";
|
||||
|
||||
.word-break-all {
|
||||
word-break: break-all !important;
|
||||
}
|
||||
|
||||
// TODO: Update edx-bootstrap theme to incorporate these edits.
|
||||
.btn, a.btn {
|
||||
text-decoration: none;
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
.btn-link {
|
||||
text-decoration: underline;
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.profile-page-bg-banner {
|
||||
height: 12rem;
|
||||
background-image: url('./assets/dot-pattern-light.png');
|
||||
background-repeat: repeat-x;
|
||||
background-size: auto 85%;
|
||||
}
|
||||
|
||||
.profile-page {
|
||||
.edit-section-header {
|
||||
@extend .h6;
|
||||
display: block;
|
||||
font-weight: normal;
|
||||
letter-spacing: 0;
|
||||
margin: 0;
|
||||
}
|
||||
label.edit-section-header {
|
||||
margin-bottom: $spacer * .5;
|
||||
}
|
||||
.profile-avatar-wrap {
|
||||
@include media-breakpoint-up(md) {
|
||||
max-width: 12rem;
|
||||
margin-right: 0;
|
||||
margin-top: -8rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.profile-avatar-menu-container {
|
||||
background: rgba(0,0,0,.65);
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 50%;
|
||||
@include media-breakpoint-up(md) {
|
||||
background: linear-gradient(to top, rgba(0,0,0,.65) 4rem, rgba(0,0,0,0) 4rem);
|
||||
align-items: flex-end;
|
||||
}
|
||||
.btn {
|
||||
text-decoration: none;
|
||||
@include media-breakpoint-up(md) {
|
||||
margin-bottom: 1.2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
@include media-breakpoint-up(md) {
|
||||
margin-bottom: 1.2rem;
|
||||
}
|
||||
.btn {
|
||||
color: $white;
|
||||
background: transparent;
|
||||
border-color: transparent;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.profile-avatar {
|
||||
width: 5rem;
|
||||
height: 5rem;
|
||||
position: relative;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
width: 12rem;
|
||||
height: 12rem;
|
||||
}
|
||||
|
||||
.profile-avatar-edit-button {
|
||||
border: none;
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding-top: .1rem;
|
||||
font-weight: 600;
|
||||
background: rgba(0,0,0,.5);
|
||||
border-radius:0;
|
||||
transition: opacity 200ms ease;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
height: 4rem;
|
||||
}
|
||||
|
||||
&:focus, &:hover, &:active, &.active {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.certificate {
|
||||
position: relative;
|
||||
.certificate-title {
|
||||
font-family: $font-family-serif;
|
||||
font-weight: 400;
|
||||
}
|
||||
.certificate-type-illustration {
|
||||
position: absolute;
|
||||
top: 1rem;
|
||||
right: 1rem;
|
||||
bottom: 0;
|
||||
width: 12rem;
|
||||
opacity: .06;
|
||||
background-size: 90%;
|
||||
background-repeat: no-repeat;
|
||||
background-position: right top;
|
||||
}
|
||||
.card-body {
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { sendTrackingLogEvent } from '@edx/frontend-analytics';
|
||||
import { App, AppContext } from '@edx/frontend-base';
|
||||
import { App, AppContext, fetchUserAccount } from '@edx/frontend-base';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-i18n';
|
||||
import { StatusAlert, Hyperlink } from '@edx/paragon';
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
openForm,
|
||||
closeForm,
|
||||
updateDraft,
|
||||
} from '../actions';
|
||||
} from './data/actions';
|
||||
|
||||
// Components
|
||||
import ProfileAvatar from './forms/ProfileAvatar';
|
||||
@@ -33,14 +33,14 @@ import PageLoading from './PageLoading';
|
||||
import Banner from './Banner';
|
||||
|
||||
// Selectors
|
||||
import { profilePageSelector } from '../selectors';
|
||||
import { profilePageSelector } from './data/selectors';
|
||||
|
||||
// i18n
|
||||
import messages from './ProfilePage.messages';
|
||||
|
||||
App.requireConfig(['CREDENTIALS_BASE_URL', 'LMS_BASE_URL'], 'ProfilePage');
|
||||
|
||||
export class ProfilePage extends React.Component {
|
||||
class ProfilePage extends React.Component {
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
@@ -58,6 +58,7 @@ export class ProfilePage extends React.Component {
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.fetchUserAccount(this.context.authenticatedUser.username);
|
||||
this.props.fetchProfile(this.props.match.params.username);
|
||||
sendTrackingLogEvent('edx.profile.viewed', {
|
||||
username: this.props.match.params.username,
|
||||
@@ -331,6 +332,7 @@ ProfilePage.propTypes = {
|
||||
photoUploadError: PropTypes.objectOf(PropTypes.string),
|
||||
|
||||
// Actions
|
||||
fetchUserAccount: PropTypes.func.isRequired,
|
||||
fetchProfile: PropTypes.func.isRequired,
|
||||
saveProfile: PropTypes.func.isRequired,
|
||||
saveProfilePhoto: PropTypes.func.isRequired,
|
||||
@@ -370,6 +372,7 @@ ProfilePage.defaultProps = {
|
||||
export default connect(
|
||||
profilePageSelector,
|
||||
{
|
||||
fetchUserAccount,
|
||||
fetchProfile,
|
||||
saveProfilePhoto,
|
||||
deleteProfilePhoto,
|
||||
@@ -7,11 +7,12 @@ import React from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
import renderer from 'react-test-renderer';
|
||||
import configureMockStore from 'redux-mock-store';
|
||||
import thunk from 'redux-thunk';
|
||||
|
||||
import messages from '../../i18n';
|
||||
import ConnectedProfilePage from './ProfilePage';
|
||||
import messages from '../i18n';
|
||||
import ProfilePage from './ProfilePage';
|
||||
|
||||
const mockStore = configureMockStore();
|
||||
const mockStore = configureMockStore([thunk]);
|
||||
const storeMocks = {
|
||||
loadingApp: require('./__mocks__/loadingApp.mockStore.js'),
|
||||
viewOwnProfile: require('./__mocks__/viewOwnProfile.mockStore.js'),
|
||||
@@ -19,6 +20,7 @@ const storeMocks = {
|
||||
savingEditedBio: require('./__mocks__/savingEditedBio.mockStore.js'),
|
||||
};
|
||||
const requiredProfilePageProps = {
|
||||
fetchUserAccount: () => {},
|
||||
fetchProfile: () => {},
|
||||
saveProfile: () => {},
|
||||
saveProfilePhoto: () => {},
|
||||
@@ -33,6 +35,7 @@ Object.defineProperty(global.document, 'cookie', {
|
||||
writable: true,
|
||||
value: `${App.config.LANGUAGE_PREFERENCE_COOKIE_NAME}=en`,
|
||||
});
|
||||
App.apiClient = jest.fn();
|
||||
|
||||
configureI18n(App.config, messages);
|
||||
|
||||
@@ -49,7 +52,7 @@ describe('<ProfilePage />', () => {
|
||||
>
|
||||
<IntlProvider locale="en">
|
||||
<Provider store={mockStore(storeMocks.loadingApp)}>
|
||||
<ConnectedProfilePage {...requiredProfilePageProps} />
|
||||
<ProfilePage {...requiredProfilePageProps} />
|
||||
</Provider>
|
||||
</IntlProvider>
|
||||
</AppContext.Provider>
|
||||
@@ -69,7 +72,7 @@ describe('<ProfilePage />', () => {
|
||||
>
|
||||
<IntlProvider locale="en">
|
||||
<Provider store={mockStore(storeMocks.viewOwnProfile)}>
|
||||
<ConnectedProfilePage {...requiredProfilePageProps} />
|
||||
<ProfilePage {...requiredProfilePageProps} />
|
||||
</Provider>
|
||||
</IntlProvider>
|
||||
</AppContext.Provider>
|
||||
@@ -89,7 +92,7 @@ describe('<ProfilePage />', () => {
|
||||
>
|
||||
<IntlProvider locale="en">
|
||||
<Provider store={mockStore(storeMocks.viewOtherProfile)}>
|
||||
<ConnectedProfilePage
|
||||
<ProfilePage
|
||||
{...requiredProfilePageProps}
|
||||
match={{ params: { username: 'verified' } }} // Override default match
|
||||
/>
|
||||
@@ -112,7 +115,7 @@ describe('<ProfilePage />', () => {
|
||||
>
|
||||
<IntlProvider locale="en">
|
||||
<Provider store={mockStore(storeMocks.savingEditedBio)}>
|
||||
<ConnectedProfilePage {...requiredProfilePageProps} />
|
||||
<ProfilePage {...requiredProfilePageProps} />
|
||||
</Provider>
|
||||
</IntlProvider>
|
||||
</AppContext.Provider>
|
||||
@@ -134,7 +137,7 @@ describe('<ProfilePage />', () => {
|
||||
>
|
||||
<IntlProvider locale="en">
|
||||
<Provider store={mockStore(storeMocks.loadingApp)}>
|
||||
<ConnectedProfilePage
|
||||
<ProfilePage
|
||||
{...requiredProfilePageProps}
|
||||
match={{ params: { username: 'test-username' } }}
|
||||
/>
|
||||
141
src/profile/_index.scss
Normal file
141
src/profile/_index.scss
Normal file
@@ -0,0 +1,141 @@
|
||||
|
||||
|
||||
$fa-font-path: "~font-awesome/fonts";
|
||||
@import "~font-awesome/scss/font-awesome";
|
||||
|
||||
.word-break-all {
|
||||
word-break: break-all !important;
|
||||
}
|
||||
|
||||
// TODO: Update edx-bootstrap theme to incorporate these edits.
|
||||
.btn, a.btn {
|
||||
text-decoration: none;
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
.btn-link {
|
||||
text-decoration: underline;
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.profile-page-bg-banner {
|
||||
height: 12rem;
|
||||
background-image: url('./assets/dot-pattern-light.png');
|
||||
background-repeat: repeat-x;
|
||||
background-size: auto 85%;
|
||||
}
|
||||
|
||||
.profile-page {
|
||||
.edit-section-header {
|
||||
@extend .h6;
|
||||
display: block;
|
||||
font-weight: normal;
|
||||
letter-spacing: 0;
|
||||
margin: 0;
|
||||
}
|
||||
label.edit-section-header {
|
||||
margin-bottom: $spacer * .5;
|
||||
}
|
||||
.profile-avatar-wrap {
|
||||
@include media-breakpoint-up(md) {
|
||||
max-width: 12rem;
|
||||
margin-right: 0;
|
||||
margin-top: -8rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.profile-avatar-menu-container {
|
||||
background: rgba(0,0,0,.65);
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 50%;
|
||||
@include media-breakpoint-up(md) {
|
||||
background: linear-gradient(to top, rgba(0,0,0,.65) 4rem, rgba(0,0,0,0) 4rem);
|
||||
align-items: flex-end;
|
||||
}
|
||||
.btn {
|
||||
text-decoration: none;
|
||||
@include media-breakpoint-up(md) {
|
||||
margin-bottom: 1.2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
@include media-breakpoint-up(md) {
|
||||
margin-bottom: 1.2rem;
|
||||
}
|
||||
.btn {
|
||||
color: $white;
|
||||
background: transparent;
|
||||
border-color: transparent;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.profile-avatar {
|
||||
width: 5rem;
|
||||
height: 5rem;
|
||||
position: relative;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
width: 12rem;
|
||||
height: 12rem;
|
||||
}
|
||||
|
||||
.profile-avatar-edit-button {
|
||||
border: none;
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding-top: .1rem;
|
||||
font-weight: 600;
|
||||
background: rgba(0,0,0,.5);
|
||||
border-radius:0;
|
||||
transition: opacity 200ms ease;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
height: 4rem;
|
||||
}
|
||||
|
||||
&:focus, &:hover, &:active, &.active {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.certificate {
|
||||
position: relative;
|
||||
.certificate-title {
|
||||
font-family: $font-family-serif;
|
||||
font-weight: 400;
|
||||
}
|
||||
.certificate-type-illustration {
|
||||
position: absolute;
|
||||
top: 1rem;
|
||||
right: 1rem;
|
||||
bottom: 0;
|
||||
width: 12rem;
|
||||
opacity: .06;
|
||||
background-size: 90%;
|
||||
background-repeat: no-repeat;
|
||||
background-position: right top;
|
||||
}
|
||||
.card-body {
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 38 KiB |
@@ -1,37 +0,0 @@
|
||||
import { AppContext, fetchUserAccount } from '@edx/frontend-base';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { useContext, useEffect } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { Route, Switch } from 'react-router-dom';
|
||||
|
||||
import NotFoundPage from './NotFoundPage';
|
||||
import ConnectedProfilePage from './ProfilePage';
|
||||
|
||||
function ProfileMain(props) {
|
||||
const { authenticatedUser } = useContext(AppContext);
|
||||
|
||||
useEffect(() => {
|
||||
props.fetchUserAccount(authenticatedUser.username);
|
||||
}, [authenticatedUser.username]);
|
||||
|
||||
return (
|
||||
<main>
|
||||
<Switch>
|
||||
<Route path="/u/:username" component={ConnectedProfilePage} />
|
||||
<Route path="/notfound" component={NotFoundPage} />
|
||||
<Route path="*" component={NotFoundPage} />
|
||||
</Switch>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
ProfileMain.propTypes = {
|
||||
fetchUserAccount: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default connect(
|
||||
null,
|
||||
{
|
||||
fetchUserAccount,
|
||||
},
|
||||
)(ProfileMain);
|
||||
@@ -13,7 +13,7 @@ import EmptyContent from './elements/EmptyContent';
|
||||
import SwitchContent from './elements/SwitchContent';
|
||||
|
||||
// Selectors
|
||||
import { editableFormSelector } from '../../selectors';
|
||||
import { editableFormSelector } from '../data/selectors';
|
||||
|
||||
class Bio extends React.Component {
|
||||
constructor(props) {
|
||||
@@ -13,11 +13,11 @@ import EditableItemHeader from './elements/EditableItemHeader';
|
||||
import SwitchContent from './elements/SwitchContent';
|
||||
|
||||
// Assets
|
||||
import professionalCertificateSVG from '../../assets/professional-certificate.svg';
|
||||
import verifiedCertificateSVG from '../../assets/verified-certificate.svg';
|
||||
import professionalCertificateSVG from '../assets/professional-certificate.svg';
|
||||
import verifiedCertificateSVG from '../assets/verified-certificate.svg';
|
||||
|
||||
// Selectors
|
||||
import { certificatesSelector } from '../../selectors';
|
||||
import { certificatesSelector } from '../data/selectors';
|
||||
|
||||
class Certificates extends React.Component {
|
||||
constructor(props) {
|
||||
@@ -13,7 +13,7 @@ import EmptyContent from './elements/EmptyContent';
|
||||
import SwitchContent from './elements/SwitchContent';
|
||||
|
||||
// Selectors
|
||||
import { countrySelector } from '../../selectors';
|
||||
import { countrySelector } from '../data/selectors';
|
||||
|
||||
class Country extends React.Component {
|
||||
constructor(props) {
|
||||
@@ -14,10 +14,10 @@ import EmptyContent from './elements/EmptyContent';
|
||||
import SwitchContent from './elements/SwitchContent';
|
||||
|
||||
// Constants
|
||||
import { EDUCATION_LEVELS } from '../../constants';
|
||||
import { EDUCATION_LEVELS } from '../data/constants';
|
||||
|
||||
// Selectors
|
||||
import { editableFormSelector } from '../../selectors';
|
||||
import { editableFormSelector } from '../data/selectors';
|
||||
|
||||
class Education extends React.Component {
|
||||
constructor(props) {
|
||||
@@ -12,7 +12,7 @@ import EmptyContent from './elements/EmptyContent';
|
||||
import SwitchContent from './elements/SwitchContent';
|
||||
|
||||
// Selectors
|
||||
import { editableFormSelector } from '../../selectors';
|
||||
import { editableFormSelector } from '../data/selectors';
|
||||
|
||||
class Name extends React.Component {
|
||||
constructor(props) {
|
||||
@@ -13,7 +13,7 @@ import EmptyContent from './elements/EmptyContent';
|
||||
import SwitchContent from './elements/SwitchContent';
|
||||
|
||||
// Selectors
|
||||
import { preferredLanguageSelector } from '../../selectors';
|
||||
import { preferredLanguageSelector } from '../data/selectors';
|
||||
|
||||
class PreferredLanguage extends React.Component {
|
||||
constructor(props) {
|
||||
@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
|
||||
import { Button, Dropdown } from '@edx/paragon';
|
||||
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-i18n';
|
||||
|
||||
import { ReactComponent as DefaultAvatar } from '../../assets/avatar.svg';
|
||||
import { ReactComponent as DefaultAvatar } from '../assets/avatar.svg';
|
||||
|
||||
import messages from './ProfileAvatar.messages';
|
||||
|
||||
@@ -16,7 +16,7 @@ import EmptyContent from './elements/EmptyContent';
|
||||
import SwitchContent from './elements/SwitchContent';
|
||||
|
||||
// Selectors
|
||||
import { editableFormSelector } from '../../selectors';
|
||||
import { editableFormSelector } from '../data/selectors';
|
||||
|
||||
const platformDisplayInfo = {
|
||||
facebook: {
|
||||
@@ -1,9 +1,5 @@
|
||||
import profileReducer from './reducers';
|
||||
import profileSaga from './sagas';
|
||||
import ConnectedProfilePage from './components/ProfilePage';
|
||||
|
||||
export {
|
||||
ConnectedProfilePage,
|
||||
profileReducer,
|
||||
profileSaga,
|
||||
};
|
||||
export { default as reducer } from './data/reducers';
|
||||
export { default as saga } from './data/sagas';
|
||||
export { default as ProfilePage } from './ProfilePage';
|
||||
export { default as NotFoundPage } from './NotFoundPage';
|
||||
export { default as messages } from './ProfilePage.messages';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { combineReducers } from 'redux';
|
||||
import { userAccount } from '@edx/frontend-auth';
|
||||
|
||||
import { profileReducer } from './profile';
|
||||
import { reducer as profileReducer } from './profile';
|
||||
|
||||
const createRootReducer = () =>
|
||||
combineReducers({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { all } from 'redux-saga/effects';
|
||||
|
||||
import { profileSaga } from './profile';
|
||||
import { saga as profileSaga } from './profile';
|
||||
|
||||
export default function* rootSaga() {
|
||||
yield all([
|
||||
|
||||
Reference in New Issue
Block a user