Skip to content

Commit

Permalink
feat: reskin of Profile MFE main page (#1114)
Browse files Browse the repository at this point in the history
* feat: reskin of Profile MFE main page

* feat: reskin of Profile MFE main page

* test: updated tests according to the changes

* fix: added missing name property

* test: updated test snapshot

* test: added tests for reducers

* feat: moved reskin logic behind env variable

* test: updated tests

* refactor: refactored code according to requested changes

* fix: fixed lint errors

* refactor: refactored code according to requested changes

* refactor: refactored code according to requested changes
  • Loading branch information
eemaanamir authored Nov 18, 2024
1 parent f3a328c commit c025310
Show file tree
Hide file tree
Showing 47 changed files with 3,417 additions and 14 deletions.
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=''
9 changes: 7 additions & 2 deletions src/data/reducers.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import { combineReducers } from 'redux';

import { reducer as profilePage } from '../profile';
import { getConfig } from '@edx/frontend-platform';

import { reducer as profilePageReducer } from '../profile';
import { reducer as newProfilePageReducer } from '../profile-v2';

const isNewProfileEnabled = getConfig().ENABLE_NEW_PROFILE_VIEW;

const createRootReducer = () => combineReducers({
profilePage,
profilePage: isNewProfileEnabled ? newProfilePageReducer : profilePageReducer,
});

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

import { getConfig } from '@edx/frontend-platform';
import { saga as profileSaga } from '../profile';
import { saga as newProfileSaga } from '../profile-v2';

const isNewProfileEnabled = getConfig().ENABLE_NEW_PROFILE_VIEW;

export default function* rootSaga() {
yield all([
profileSaga(),
isNewProfileEnabled ? newProfileSaga() : profileSaga(),
]);
}
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';
13 changes: 10 additions & 3 deletions src/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
initialize,
mergeConfig,
subscribe,
getConfig,
} from '@edx/frontend-platform';
import {
AppProvider,
Expand All @@ -22,18 +23,23 @@ import FooterSlot from '@openedx/frontend-slot-footer';
import messages from './i18n';
import configureStore from './data/configureStore';

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

import AppRoutes from './routes/AppRoutes';

subscribe(APP_READY, () => {
subscribe(APP_READY, async () => {
const isNewProfileEnabled = getConfig().ENABLE_NEW_PROFILE_VIEW === 'true';
if (isNewProfileEnabled) {
await import('./index-v2.scss');
} else {
await import('./index.scss');
}
ReactDOM.render(
<AppProvider store={configureStore()}>
<Head />
<Header />
<main id="main">
<AppRoutes />
<AppRoutes isNewProfileEnabled={isNewProfileEnabled} />
</main>
<FooterSlot />
</AppProvider>,
Expand All @@ -53,6 +59,7 @@ initialize({
mergeConfig({
COLLECT_YEAR_OF_BIRTH: process.env.COLLECT_YEAR_OF_BIRTH,
ENABLE_SKILLS_BUILDER_PROFILE: process.env.ENABLE_SKILLS_BUILDER_PROFILE,
ENABLE_NEW_PROFILE_VIEW: process.env.ENABLE_NEW_PROFILE_VIEW || null,
}, 'App loadConfig override handler');
},
},
Expand Down
111 changes: 111 additions & 0 deletions src/profile-v2/CertificateCard.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import React from 'react';
import PropTypes from 'prop-types';
import { FormattedDate, FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
import { Hyperlink } from '@openedx/paragon';
import get from 'lodash.get';

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

const CertificateCard = ({
certificateType,
courseDisplayName,
courseOrganization,
modifiedDate,
downloadUrl,
courseId,
uuid,
}) => {
const intl = useIntl();

const certificateIllustration = {
professional: professionalCertificateSVG,
'no-id-professional': professionalCertificateSVG,
verified: verifiedCertificateSVG,
honor: null,
audit: null,
}[certificateType] || null;

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="d-flex flex-column position-relative 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>
<h4 className="m-0 color-black">{courseDisplayName}</h4>
<p className="small mb-0">
<FormattedMessage
id="profile.certificate.organization.label"
defaultMessage="From"
/>
</p>
<h5 className="mb-0 color-black">{courseOrganization}</h5>
<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>
);
};

CertificateCard.propTypes = {
certificateType: PropTypes.string,
courseDisplayName: PropTypes.string,
courseOrganization: PropTypes.string,
modifiedDate: PropTypes.string,
downloadUrl: PropTypes.string,
courseId: PropTypes.string.isRequired,
uuid: PropTypes.string,
};

CertificateCard.defaultProps = {
certificateType: 'unknown',
courseDisplayName: '',
courseOrganization: '',
modifiedDate: '',
downloadUrl: '',
uuid: '',
};

export default CertificateCard;
74 changes: 74 additions & 0 deletions src/profile-v2/Certificates.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import React from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import { connect } from 'react-redux';
import { getConfig } from '@edx/frontend-platform';

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

const Certificates = ({ certificates }) => (
<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>
{certificates?.length > 0 ? (
<div className="col">
<div className="row align-items-center pt-5 g-3rem">
{certificates.map(certificate => (
<CertificateCard key={certificate.courseId} {...certificate} />
))}
</div>
</div>
) : (
<div className="pt-5">
<FormattedMessage
id="profile.no.certificates"
defaultMessage="You don't have any certificates yet."
description="displays when user has no course completion certificates"
/>
</div>
)}
</div>
);

Certificates.propTypes = {
certificates: PropTypes.arrayOf(PropTypes.shape({
certificateType: PropTypes.string,
courseDisplayName: PropTypes.string,
courseOrganization: PropTypes.string,
modifiedDate: PropTypes.string,
downloadUrl: PropTypes.string,
courseId: PropTypes.string.isRequired,
uuid: PropTypes.string,
})),
};

Certificates.defaultProps = {
certificates: [],
};

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

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

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 memo(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">
<p className="my-0 py-5 text-muted max-width-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;
18 changes: 18 additions & 0 deletions src/profile-v2/PageLoading.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import React from 'react';
import PropTypes from 'prop-types';

const PageLoading = ({ srMessage }) => (
<div>
<div className="d-flex justify-content-center align-items-center flex-column height-50vh">
<div className="spinner-border text-primary" role="status">
{srMessage && <span className="sr-only">{srMessage}</span>}
</div>
</div>
</div>
);

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

export default PageLoading;
Loading

0 comments on commit c025310

Please sign in to comment.