Skip to content

Commit

Permalink
feat: reskin of Profile MFE main page
Browse files Browse the repository at this point in the history
  • Loading branch information
eemaanamir committed Oct 24, 2024
1 parent ca2a79d commit 38b8019
Show file tree
Hide file tree
Showing 42 changed files with 16,810 additions and 4 deletions.
2 changes: 1 addition & 1 deletion src/data/reducers.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { combineReducers } from 'redux';

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

const createRootReducer = () => combineReducers({
profilePage,
Expand Down
2 changes: 1 addition & 1 deletion src/data/sagas.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { all } from 'redux-saga/effects';

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

export default function* rootSaga() {
yield all([
Expand Down
2 changes: 1 addition & 1 deletion src/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@
@import "~@edx/frontend-component-header/dist/index";
@import "~@edx/frontend-component-footer/dist/footer";

@import './profile/index';
@import './profile-v2/index';
43 changes: 43 additions & 0 deletions src/profile-v2/AgeMessage.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Alert } from '@openedx/paragon';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import { getConfig } from '@edx/frontend-platform';

const AgeMessage = ({ accountSettingsUrl }) => (
<Alert
variant="info"
dismissible={false}
show
>
<Alert.Heading id="profile.age.headline">
<FormattedMessage
id="profile.age.cannotShare"
defaultMessage="Your profile cannot be shared."
description="Error message indicating that the user's profile cannot be shared"
/>
</Alert.Heading>
<FormattedMessage
id="profile.age.details"
defaultMessage="To share your profile with other {siteName} learners, you must confirm that you are over the age of 13."
description="Error message"
tagName="p"
values={{
siteName: getConfig().SITE_NAME,
}}
/>
<Alert.Link href={accountSettingsUrl}>
<FormattedMessage
id="profile.age.set.date"
defaultMessage="Set your date of birth"
description="Label on a link to set birthday"
/>
</Alert.Link>
</Alert>
);

AgeMessage.propTypes = {
accountSettingsUrl: PropTypes.string.isRequired,
};

export default AgeMessage;
5 changes: 5 additions & 0 deletions src/profile-v2/Banner.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import React from 'react';

const Banner = () => <div className="profile-page-bg-banner bg-primary d-md-block p-relative" />;

export default Banner;
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;
163 changes: 163 additions & 0 deletions src/profile-v2/Certificates.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
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();

// Memoizing the renderCertificate function to prevent unnecessary re-renders
const renderCertificate = useCallback(({
certificateType, courseDisplayName, courseOrganization, modifiedDate, downloadUrl, courseId,
}) => {
const certificateIllustration = (() => {
switch (certificateType) {
case 'professional':
case 'no-id-professional':
return professionalCertificateSVG;
case 'verified':
return verifiedCertificateSVG;
case 'honor':
case 'audit':
default:
return 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="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>
</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;
}

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">
<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;
}

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

0 comments on commit 38b8019

Please sign in to comment.