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=''
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';
eemaanamir marked this conversation as resolved.
Show resolved Hide resolved

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
eemaanamir marked this conversation as resolved.
Show resolved Hide resolved

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';
eemaanamir marked this conversation as resolved.
Show resolved Hide resolved

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
eemaanamir marked this conversation as resolved.
Show resolved Hide resolved

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') {
eemaanamir marked this conversation as resolved.
Show resolved Hide resolved
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 }) => {
eemaanamir marked this conversation as resolved.
Show resolved Hide resolved
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>
);
};
eemaanamir marked this conversation as resolved.
Show resolved Hide resolved

CertificateCount.propTypes = {
count: PropTypes.number,
};
CertificateCount.defaultProps = {
count: 0,
};
eemaanamir marked this conversation as resolved.
Show resolved Hide resolved

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';
eemaanamir marked this conversation as resolved.
Show resolved Hide resolved
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
eemaanamir marked this conversation as resolved.
Show resolved Hide resolved
import professionalCertificateSVG from './assets/professional-certificate.svg';
import verifiedCertificateSVG from './assets/verified-certificate.svg';

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

const Certificates = ({
certificates,
}) => {
eemaanamir marked this conversation as resolved.
Show resolved Hide resolved
const intl = useIntl();

const renderCertificate = useCallback(({
eemaanamir marked this conversation as resolved.
Show resolved Hide resolved
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
eemaanamir marked this conversation as resolved.
Show resolved Hide resolved
}
})();

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>
eemaanamir marked this conversation as resolved.
Show resolved Hide resolved
<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(() => {
eemaanamir marked this conversation as resolved.
Show resolved Hide resolved
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>
);
eemaanamir marked this conversation as resolved.
Show resolved Hide resolved
}, [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,
})),
};
eemaanamir marked this conversation as resolved.
Show resolved Hide resolved

Certificates.defaultProps = {
certificates: null,
};
eemaanamir marked this conversation as resolved.
Show resolved Hide resolved

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;
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
}
eemaanamir marked this conversation as resolved.
Show resolved Hide resolved

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;
eemaanamir marked this conversation as resolved.
Show resolved Hide resolved
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' }}>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think we don't need to pass maxWidth here. try it will full width

<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 {
eemaanamir marked this conversation as resolved.
Show resolved Hide resolved
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()}
eemaanamir marked this conversation as resolved.
Show resolved Hide resolved
</div>
</div>
</div>
);
}
}

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