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

fix: fit & finish profile card plugin #870

Merged
merged 19 commits into from
Oct 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
a6e63a8
chore(deps): update dependency glob to v10.3.10
renovate[bot] Oct 2, 2023
9623f56
build: create profile plugin page
jsnwesson Oct 3, 2023
f08fc75
build: wrap Profile Plugin Page with Plugin
jsnwesson Oct 4, 2023
5e641d7
build: add plugins folder to Profile
jsnwesson Oct 4, 2023
095b91c
chore: update browserslist DB
jsnwesson Oct 9, 2023
896905b
fix(deps): update dependency @edx/frontend-component-footer to v12.3.0
renovate[bot] Oct 9, 2023
b892ba7
fix(deps): update dependency @edx/frontend-component-header to v4.7.1
renovate[bot] Oct 9, 2023
43f485d
Merge pull request #850 from openedx/update-browserslist-db
deborahgu Oct 10, 2023
97eb7be
Merge branch 'master' into jwesson/plugin-profile-page
jsnwesson Oct 12, 2023
06ed2d2
fix: add correct path to logging
jsnwesson Oct 12, 2023
6cedecc
revert: remove update to react-intl and keep lint error fix
jsnwesson Oct 13, 2023
f4171bf
Merge branch 'ProfilePluginPOC' of github.com:openedx/frontend-app-pr…
jsnwesson Oct 13, 2023
d94697a
fix: begin prettifying profile plugin by removing form features
jsnwesson Oct 13, 2023
99f5e94
fix: lint errors in plugin page
jsnwesson Oct 13, 2023
ebee54e
fix: format plugin elements into card
jsnwesson Oct 13, 2023
e12a7ba
fix: move Profile Plugin into card format
jsnwesson Oct 16, 2023
7356674
fix: add icon and styling to location
jsnwesson Oct 17, 2023
661374f
fix: align card footer items and move avatar over card section
jsnwesson Oct 17, 2023
5e65c5f
fix: clean and refactor Profile Plugin
jsnwesson Oct 17, 2023
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
3 changes: 3 additions & 0 deletions plugins/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,6 @@ export {
export {
IFRAME_PLUGIN,
} from './data/constants';
export {
default as PluginErrorBoundary,
} from './PluginErrorBoundary';
264 changes: 102 additions & 162 deletions src/profile/ProfilePluginPage.jsx
Original file line number Diff line number Diff line change
@@ -1,38 +1,38 @@
/* eslint-disable react/prop-types */
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';

import { ensureConfig } from '@edx/frontend-platform';
import { AppContext, ErrorBoundary } from '@edx/frontend-platform/react';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { AppContext } from '@edx/frontend-platform/react';
import { injectIntl, intlShape, FormattedDate } from '@edx/frontend-platform/i18n';

import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faTwitter, faFacebook, faLinkedin } from '@fortawesome/free-brands-svg-icons';
import {
ActionRow, Avatar, Card, Hyperlink, Icon,
} from '@edx/paragon';
import { HistoryEdu, VerifiedUser } from '@edx/paragon/icons';

import get from 'lodash.get';

import PluginCountry from './forms/PluginCountry';
import { Plugin } from '../../plugins';

// Actions
import {
fetchProfile,
saveProfile,
saveProfilePhoto,
deleteProfilePhoto,
openForm,
closeForm,
updateDraft,
} from './data/actions';

// Components
import ProfileAvatar from './forms/ProfileAvatar';
import Name from './forms/Name';
import Country from './forms/Country';
import Education from './forms/Education';
import SocialLinks from './forms/SocialLinks';
import Bio from './forms/Bio';
import DateJoined from './DateJoined';
import PageLoading from './PageLoading';

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

// i18n
import messages from './ProfilePage.messages';
import eduMessages from './forms/Education.messages';

import withParams from '../utils/hoc';

Expand All @@ -45,143 +45,99 @@ function Fallback() {
);
}

class ProfilePluginPage extends React.Component {
constructor(props, context) {
super(props, context);

this.handleSaveProfilePhoto = this.handleSaveProfilePhoto.bind(this);
this.handleDeleteProfilePhoto = this.handleDeleteProfilePhoto.bind(this);
this.handleClose = this.handleClose.bind(this);
this.handleOpen = this.handleOpen.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
this.handleChange = this.handleChange.bind(this);
}
const platformDisplayInfo = {
facebook: {
icon: faFacebook,
name: '',
},
twitter: {
icon: faTwitter,
name: '',
},
linkedin: {
icon: faLinkedin,
name: '',
},
};

class ProfilePluginPage extends React.Component {
componentDidMount() {
this.props.fetchProfile(this.props.params.username);
}

handleSaveProfilePhoto(formData) {
this.props.saveProfilePhoto(this.context.authenticatedUser.username, formData);
}

handleDeleteProfilePhoto() {
this.props.deleteProfilePhoto(this.context.authenticatedUser.username);
}

handleClose(formId) {
this.props.closeForm(formId);
}

handleOpen(formId) {
this.props.openForm(formId);
}

handleSubmit(formId) {
this.props.saveProfile(formId, this.context.authenticatedUser.username);
}

handleChange(name, value) {
this.props.updateDraft(name, value);
}

// Inserted into the DOM in two places (for responsive layout)
renderHeadingLockup() {
const { dateJoined } = this.props;
return (
<ErrorBoundary fallbackComponent={<Fallback />}>
<span data-hj-suppress>
<h1 className="h2 mb-0 font-weight-bold">{this.props.params.username}</h1>
<DateJoined date={dateJoined} />
<hr className="d-none d-md-block" />
</span>
</ErrorBoundary>
);
}

renderContent() {
const {
profileImage,
name,
visibilityName,
country,
visibilityCountry,
levelOfEducation,
visibilityLevelOfEducation,
socialLinks,
visibilitySocialLinks,
bio,
visibilityBio,
isLoadingProfile,
dateJoined,
intl,
} = this.props;

if (isLoadingProfile) {
return <PageLoading srMessage={this.props.intl.formatMessage(messages['profile.loading'])} />;
}

const commonFormProps = {
openHandler: this.handleOpen,
closeHandler: this.handleClose,
submitHandler: this.handleSubmit,
changeHandler: this.handleChange,
};

return (
<Plugin>
<div className="container-fluid">
<div className="row align-items-center pt-4 mb-4 pt-md-0 mb-md-0">
<div className="col-auto col-md-4 col-lg-3">
<div className="d-flex align-items-center d-md-block">
<ProfileAvatar
className="mb-md-3"
src={profileImage.src}
isDefault={profileImage.isDefault}
/>
</div>
</div>
<div className="col pl-0">
<div>
{this.renderHeadingLockup()}
</div>
</div>
</div>
<div className="row">
<div className="col-md-4 col-lg-4">
<Name
name={name}
visibilityName={visibilityName}
formId="name"
{...commonFormProps}
/>
<Country
country={country}
visibilityCountry={visibilityCountry}
formId="country"
{...commonFormProps}
/>
<Education
levelOfEducation={levelOfEducation}
visibilityLevelOfEducation={visibilityLevelOfEducation}
formId="levelOfEducation"
{...commonFormProps}
/>
<SocialLinks
socialLinks={socialLinks}
visibilitySocialLinks={visibilitySocialLinks}
formId="socialLinks"
{...commonFormProps}
/>
</div>
<div className="pt-md-3 col-md-8 col-lg-7 offset-lg-1">
<Bio
bio={bio}
visibilityBio={visibilityBio}
formId="bio"
{...commonFormProps}
/>
</div>
</div>
</div>
<Plugin fallbackComponent={<Fallback />}>
<Card className="mb-2">
<Card.Header
className="pb-5"
subtitle={(
<Hyperlink destination={`http://localhost:1995/u/${this.props.params.username}`}>
View public profile
</Hyperlink>
)}
actions={
(
<ActionRow className="mt-3">
{socialLinks
.filter(({ socialLink }) => Boolean(socialLink))
.map(({ platform, socialLink }) => (
<StaticListItem
key={platform}
name={platformDisplayInfo[platform].name}
url={socialLink}
platform={platform}
/>
))}
</ActionRow>
)
}
/>
<Card.Section className="text-center" muted>
<Avatar
size="xl"
className="profile-plugin-avatar"
src={profileImage.src}
alt="Profile image"
/>
<h1 className="h2 mb-0 font-weight-bold">{this.props.params.username}</h1>
<PluginCountry
country={country}
/>
</Card.Section>
<Card.Footer className="p-0">
<Card.Section className="pgn-icons-cell-vertical">
<Icon src={VerifiedUser} />
<p>
since <FormattedDate value={new Date(dateJoined)} year="numeric" />
</p>
</Card.Section>
<Card.Section className="pgn-icons-cell-vertical">
<Icon src={HistoryEdu} />
<p>
{intl.formatMessage(get(
eduMessages,
`profile.education.levels.${levelOfEducation}`,
eduMessages['profile.education.levels.o'],
))}
</p>
</Card.Section>
</Card.Footer>
</Card>
</Plugin>
);
}
Expand All @@ -195,52 +151,46 @@ class ProfilePluginPage extends React.Component {
}
}

const SocialLink = ({ url, name, platform }) => (
<a href={url} className="font-weight-bold">
<FontAwesomeIcon className="mr-2" icon={platformDisplayInfo[platform].icon} />
{name}
</a>
);

const StaticListItem = ({ url, name, platform }) => (
<ul className="list-inline">
<SocialLink name={name} url={url} platform={platform} />
</ul>
);

ProfilePluginPage.contextType = AppContext;

ProfilePluginPage.propTypes = {
// Account data
dateJoined: PropTypes.string,

// Bio form data
bio: PropTypes.string,
yearOfBirth: PropTypes.number,
visibilityBio: PropTypes.string.isRequired,

// Country form data
country: PropTypes.string,
visibilityCountry: PropTypes.string.isRequired,

// Education form data
levelOfEducation: PropTypes.string,
visibilityLevelOfEducation: PropTypes.string.isRequired,

// Name form data
name: PropTypes.string,
visibilityName: PropTypes.string.isRequired,

// Social links form data
socialLinks: PropTypes.arrayOf(PropTypes.shape({
platform: PropTypes.string,
socialLink: PropTypes.string,
})),
visibilitySocialLinks: PropTypes.string.isRequired,

// Other data we need
profileImage: PropTypes.shape({
src: PropTypes.string,
isDefault: PropTypes.bool,
}),
saveState: PropTypes.oneOf([null, 'pending', 'complete', 'error']),
isLoadingProfile: PropTypes.bool.isRequired,

// Actions
fetchProfile: PropTypes.func.isRequired,
saveProfile: PropTypes.func.isRequired,
saveProfilePhoto: PropTypes.func.isRequired,
deleteProfilePhoto: PropTypes.func.isRequired,
openForm: PropTypes.func.isRequired,
closeForm: PropTypes.func.isRequired,
updateDraft: PropTypes.func.isRequired,

// Router
params: PropTypes.shape({
Expand All @@ -252,26 +202,16 @@ ProfilePluginPage.propTypes = {
};

ProfilePluginPage.defaultProps = {
saveState: null,
profileImage: {},
name: null,
yearOfBirth: null,
levelOfEducation: null,
country: null,
socialLinks: [],
bio: null,
dateJoined: null,
};

export default connect(
profilePageSelector,
{
fetchProfile,
saveProfilePhoto,
deleteProfilePhoto,
saveProfile,
openForm,
closeForm,
updateDraft,
},
)(injectIntl(withParams(ProfilePluginPage)));
Loading