Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: reskin of Profile MFE main page #1114

Open
wants to merge 8 commits into
base: 2u-main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .env
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,4 @@ APP_ID=''
MFE_CONFIG_API_URL=''
SEARCH_CATALOG_URL=''
ENABLE_SKILLS_BUILDER_PROFILE=''
ENABLE_NEW_PROFILE_VIEW=''
1 change: 1 addition & 0 deletions .env.development
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,4 @@ APP_ID=''
MFE_CONFIG_API_URL=''
SEARCH_CATALOG_URL='http://localhost:18000/courses'
ENABLE_SKILLS_BUILDER_PROFILE=''
ENABLE_NEW_PROFILE_VIEW=''
1 change: 1 addition & 0 deletions .env.test
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,4 @@ LEARNER_RECORD_MFE_BASE_URL='http://localhost:1990'
COLLECT_YEAR_OF_BIRTH=true
APP_ID=''
MFE_CONFIG_API_URL=''
ENABLE_NEW_PROFILE_VIEW=''
5 changes: 4 additions & 1 deletion src/data/reducers.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { combineReducers } from 'redux';

import { reducer as profilePage } from '../profile';
import { reducer as NewProfilePageReducer } from '../profile-v2';

const isNewProfileEnabled = process.env.ENABLE_NEW_PROFILE_VIEW === 'true';

Check warning on line 6 in src/data/reducers.js

View check run for this annotation

Codecov / codecov/patch

src/data/reducers.js#L6

Added line #L6 was not covered by tests

const createRootReducer = () => combineReducers({
profilePage,
profilePage: isNewProfileEnabled ? NewProfilePageReducer : profilePage,

Check warning on line 9 in src/data/reducers.js

View check run for this annotation

Codecov / codecov/patch

src/data/reducers.js#L9

Added line #L9 was not covered by tests
});

export default createRootReducer;
6 changes: 4 additions & 2 deletions src/data/sagas.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { all } from 'redux-saga/effects';

import { saga as profileSaga } from '../profile';
import { saga as NewProfileSaga } from '../profile-v2';

const isNewProfileEnabled = process.env.ENABLE_NEW_PROFILE_VIEW === 'true';

Check warning on line 5 in src/data/sagas.js

View check run for this annotation

Codecov / codecov/patch

src/data/sagas.js#L5

Added line #L5 was not covered by tests

export default function* rootSaga() {
yield all([
profileSaga(),
isNewProfileEnabled ? NewProfileSaga() : profileSaga(),

Check warning on line 9 in src/data/sagas.js

View check run for this annotation

Codecov / codecov/patch

src/data/sagas.js#L9

Added line #L9 was not covered by tests
]);
}
8 changes: 8 additions & 0 deletions src/index-v2.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
@import "~@edx/brand/paragon/fonts";
@import "~@edx/brand/paragon/variables";
@import "~@openedx/paragon/scss/core/core";
@import "~@edx/brand/paragon/overrides";
@import "~@edx/frontend-component-header/dist/index";
@import "~@edx/frontend-component-footer/dist/footer";

@import './profile-v2/index';
7 changes: 6 additions & 1 deletion src/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,16 @@
import messages from './i18n';
import configureStore from './data/configureStore';

import './index.scss';
import Head from './head/Head';

import AppRoutes from './routes/AppRoutes';

if (process.env.ENABLE_NEW_PROFILE_VIEW === 'true') {
import('./index-v2.scss');
} else {
import('./index.scss');

Check warning on line 32 in src/index.jsx

View check run for this annotation

Codecov / codecov/patch

src/index.jsx#L30-L32

Added lines #L30 - L32 were not covered by tests
}

subscribe(APP_READY, () => {
ReactDOM.render(
<AppProvider store={configureStore()}>
Expand Down
31 changes: 31 additions & 0 deletions src/profile-v2/CertificateCount.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import React from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from '@edx/frontend-platform/i18n';

const CertificateCount = ({ count }) => {
if (count === 0) {
return null;
}

return (
<span className="small m-0 text-gray-800">
<FormattedMessage
id="profile.certificatecount"
defaultMessage="{certificate_count} certifications"
description="A label for many certificates a user has"
values={{
certificate_count: <span className="font-weight-bold"> {count} </span>,
}}
/>
</span>
);
};

CertificateCount.propTypes = {
count: PropTypes.number,
};
CertificateCount.defaultProps = {
count: 0,
};

export default CertificateCount;
171 changes: 171 additions & 0 deletions src/profile-v2/Certificates.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
import React, { useCallback, useMemo } from 'react';
import PropTypes from 'prop-types';
import {
FormattedDate, FormattedMessage, useIntl,
} from '@edx/frontend-platform/i18n';
import { Hyperlink } from '@openedx/paragon';
import { connect } from 'react-redux';
import get from 'lodash.get';

import { getConfig } from '@edx/frontend-platform';
import messages from './Certificates.messages';

// Assets
import professionalCertificateSVG from './assets/professional-certificate.svg';
import verifiedCertificateSVG from './assets/verified-certificate.svg';

// Selectors
import { certificatesSelector } from './data/selectors';

const Certificates = ({
certificates,
}) => {
const intl = useIntl();

const renderCertificate = useCallback(({
certificateType, courseDisplayName, courseOrganization, modifiedDate, downloadUrl, courseId, uuid,
}) => {
const certificateIllustration = (() => {
switch (certificateType) {
case 'professional':
case 'no-id-professional':
return professionalCertificateSVG;

Check warning on line 32 in src/profile-v2/Certificates.jsx

View check run for this annotation

Codecov / codecov/patch

src/profile-v2/Certificates.jsx#L30-L32

Added lines #L30 - L32 were not covered by tests
case 'verified':
return verifiedCertificateSVG;
case 'honor':
case 'audit':
default:
return null;

Check warning on line 38 in src/profile-v2/Certificates.jsx

View check run for this annotation

Codecov / codecov/patch

src/profile-v2/Certificates.jsx#L35-L38

Added lines #L35 - L38 were not covered by tests
}
})();

return (
<div
key={`${modifiedDate}-${courseId}`}
className="col-auto d-flex align-items-center p-0"
>
<div className="col certificate p-4 border-light-400 bg-light-200 w-100 h-100">
<div
className="certificate-type-illustration"
style={{ backgroundImage: `url(${certificateIllustration})` }}
/>
<div className="card-body d-flex flex-column p-0 width-19625rem">
<div className="w-100 color-black">
<p className="small mb-0 font-weight-normal">
{intl.formatMessage(get(
messages,
`profile.certificates.types.${certificateType}`,
messages['profile.certificates.types.unknown'],
))}
</p>
<div className="h4 m-0 line-height-1575rem">{courseDisplayName}</div>
<p className="small mb-0">
<FormattedMessage
id="profile.certificate.organization.label"
defaultMessage="From"
/>
</p>
<p className="h5 mb-0">{courseOrganization}</p>
<p className="small mb-0">
<FormattedMessage
id="profile.certificate.completion.date.label"
defaultMessage="Completed on {date}"
values={{
date: <FormattedDate value={new Date(modifiedDate)} />,
}}
/>
</p>
</div>
<div className="pt-3">
<Hyperlink
destination={downloadUrl}
target="_blank"
showLaunchIcon={false}
className="btn btn-primary btn-rounded font-weight-normal px-4 py-0625rem"
>
{intl.formatMessage(messages['profile.certificates.view.certificate'])}
</Hyperlink>
</div>
<p className="small mb-0 pt-3">
<FormattedMessage
id="profile.certificate.uuid"
defaultMessage="Credential ID {certificate_uuid}"
values={{
certificate_uuid: uuid,
}}
/>
</p>
</div>
</div>
</div>
);
}, [intl]);

// Memoizing the renderCertificates to avoid recalculations
const renderCertificates = useMemo(() => {
if (!certificates || 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="col">
<div className="row align-items-center pt-5 g-3rem">
{certificates.map(certificate => renderCertificate(certificate))}
</div>
</div>
);
}, [certificates, renderCertificate]);

// Main Render
return (
<div>
<div className="col justify-content-start align-items-start g-5rem p-0">
<div className="col align-self-stretch height-2625rem justify-content-start align-items-start p-0">
<h2 className="font-weight-bold text-primary-500 m-0">
<FormattedMessage
id="profile.your.certificates"
defaultMessage="Your certificates"
description="heading for the certificates section"
/>
</h2>
</div>
<div className="col justify-content-start align-items-start pt-2 p-0">
<p className="font-weight-normal text-gray-800 m-0 p-0">
<FormattedMessage
id="profile.certificates.description"
defaultMessage="Your learner records information is only visible to you. Only your username is visible to others on {siteName}."
description="description of the certificates section"
values={{
siteName: getConfig().SITE_NAME,
}}
/>
</p>
</div>
</div>
{renderCertificates}
</div>
);
};

Certificates.propTypes = {

// From Selector
certificates: PropTypes.arrayOf(PropTypes.shape({
title: PropTypes.string,
})),
};

Certificates.defaultProps = {
certificates: null,
};

export default connect(
certificatesSelector,
{},
)(Certificates);
31 changes: 31 additions & 0 deletions src/profile-v2/Certificates.messages.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { defineMessages } from '@edx/frontend-platform/i18n';

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;
31 changes: 31 additions & 0 deletions src/profile-v2/DateJoined.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import React from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage, FormattedDate } from '@edx/frontend-platform/i18n';

const DateJoined = ({ date }) => {
if (date == null) {
return null;

Check warning on line 7 in src/profile-v2/DateJoined.jsx

View check run for this annotation

Codecov / codecov/patch

src/profile-v2/DateJoined.jsx#L7

Added line #L7 was not covered by tests
}

return (
<span className="small mb-0 text-gray-800">
<FormattedMessage
id="profile.datejoined.member.since"
defaultMessage="Member since {year}"
description="A label for how long the user has been a member"
values={{
year: <span className="font-weight-bold"> <FormattedDate value={new Date(date)} year="numeric" /> </span>,
}}
/>
</span>
);
};

DateJoined.propTypes = {
date: PropTypes.string,
};
DateJoined.defaultProps = {
date: null,
};

export default DateJoined;
16 changes: 16 additions & 0 deletions src/profile-v2/NotFoundPage.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import React from 'react';
import { FormattedMessage } from '@edx/frontend-platform/i18n';

const NotFoundPage = () => (
<div className="container-fluid d-flex py-5 justify-content-center align-items-start text-center">

Check warning on line 5 in src/profile-v2/NotFoundPage.jsx

View check run for this annotation

Codecov / codecov/patch

src/profile-v2/NotFoundPage.jsx#L4-L5

Added lines #L4 - L5 were not covered by tests
<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 NotFoundPage;
37 changes: 37 additions & 0 deletions src/profile-v2/PageLoading.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';

export default class PageLoading extends Component {
renderSrMessage() {
if (!this.props.srMessage) {
return null;

Check warning on line 7 in src/profile-v2/PageLoading.jsx

View check run for this annotation

Codecov / codecov/patch

src/profile-v2/PageLoading.jsx#L7

Added line #L7 was not covered by tests
}

return (
<span className="sr-only">
{this.props.srMessage}
</span>
);
}

render() {
return (
<div>
<div
className="d-flex justify-content-center align-items-center flex-column"
style={{
height: '50vh',
}}
>
<div className="spinner-border text-primary" role="status">
{this.renderSrMessage()}
</div>
</div>
</div>
);
}
}

PageLoading.propTypes = {
srMessage: PropTypes.string.isRequired,
};
Loading