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

Merged
merged 12 commits into from
Nov 18, 2024
Merged
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;

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

View check run for this annotation

Codecov / codecov/patch

src/data/reducers.js#L8

Added line #L8 was not covered by tests

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

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

View check run for this annotation

Codecov / codecov/patch

src/data/reducers.js#L11

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

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;

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

View check run for this annotation

Codecov / codecov/patch

src/data/sagas.js#L6

Added line #L6 was not covered by tests

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

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

View check run for this annotation

Codecov / codecov/patch

src/data/sagas.js#L10

Added line #L10 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';
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 @@
initialize,
mergeConfig,
subscribe,
getConfig,
} from '@edx/frontend-platform';
import {
AppProvider,
Expand All @@ -22,18 +23,23 @@
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';

Check warning on line 31 in src/index.jsx

View check run for this annotation

Codecov / codecov/patch

src/index.jsx#L30-L31

Added lines #L30 - L31 were not covered by tests
if (isNewProfileEnabled) {
await import('./index-v2.scss');
} else {
await import('./index.scss');

Check warning on line 35 in src/index.jsx

View check run for this annotation

Codecov / codecov/patch

src/index.jsx#L33-L35

Added lines #L33 - L35 were not covered by tests
}
ReactDOM.render(
<AppProvider store={configureStore()}>
<Head />
<Header />
<main id="main">
<AppRoutes />
<AppRoutes isNewProfileEnabled={isNewProfileEnabled} />
</main>
<FooterSlot />
</AppProvider>,
Expand All @@ -53,6 +59,7 @@
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;

Check warning on line 28 in src/profile-v2/CertificateCard.jsx

View check run for this annotation

Codecov / codecov/patch

src/profile-v2/CertificateCard.jsx#L28

Added line #L28 was 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="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': {
eemaanamir marked this conversation as resolved.
Show resolved Hide resolved
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">

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 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