diff --git a/src/app.test.js b/src/app.test.js index e4e063ed..696b200e 100644 --- a/src/app.test.js +++ b/src/app.test.js @@ -38,7 +38,7 @@ describe('Application', () => { '/s': 'Search results for map search', '/l/listing-title-slug/1234': 'Loading listing…', '/l/1234': 'Loading listing…', - '/u/1234': 'Profile page with display name: 1234', + '/u/1234': 'Profile page', '/login': 'Log in', '/signup': 'Sign up', '/recover-password': 'Request a new password', diff --git a/src/containers/ProfilePage/ProfilePage.css b/src/containers/ProfilePage/ProfilePage.css new file mode 100644 index 00000000..864b65ec --- /dev/null +++ b/src/containers/ProfilePage/ProfilePage.css @@ -0,0 +1,121 @@ +@import '../../marketplace.css'; + +.loading, +.error { + margin-top: 5px; + + @media (--viewportMedium) { + margin-top: 4px; + } + @media (--viewportLarge) { + margin-top: 3px; + } +} + +.error { + color: var(--failColor); +} + +.aside { + box-shadow: none; +} + +.asideContent { + width: 100%; + display: flex; + flex-direction: row; + border-bottom: 1px solid var(--matterColorNegative); + + @media (--viewportLarge) { + flex-direction: column; + border-bottom: none; + } +} + +.avatar { + margin: 30px 26px 29px 0; + flex-shrink: 0; + + @media (--viewportLarge) { + margin: 0 96px 44px 0; + } +} + +.mobileHeading { + flex-shrink: 0; + margin: 47px 0 0 0; + + @media (--viewportMedium) { + margin: 49px 0 0 0; + } + @media (--viewportLarge) { + display: none; + } +} + +.editLinkMobile { + margin-top: 17px; + + /* Pull the link to the end of the container */ + margin-left: auto; + + @media (--viewportMedium) { + margin-top: 20px; + } + @media (--viewportLarge) { + display: none; + } +} + +.editLinkDesktop { + display: none; + + @media (--viewportLarge) { + display: inline; + } +} + +.desktopHeading { + display: none; + + @media (--viewportLarge) { + display: block; + margin: 4px 0 23px 0; + } +} + +.bio { + /* Preserve newlines, but collapse other whitespace */ + white-space: pre-line; + + max-width: 563px; + margin: 5px 0 24px 0; + + @media (--viewportMedium) { + margin: 4px 0 51px 0; + } + @media (--viewportLarge) { + margin: 0 0 56px 0; + } +} + +.listingsContainer { + border-top: 1px solid var(--matterColorNegative); +} + +.listingsTitle { + @apply --marketplaceH3FontStyles; + color: var(--matterColorAnti); + + @media (--viewportLarge) { + margin-top: 19px; + } +} + +.withBioMissingAbove { + border-top: none; + + @media (--viewportLarge) { + border-top: 1px solid var(--matterColorNegative); + } +} diff --git a/src/containers/ProfilePage/ProfilePage.duck.js b/src/containers/ProfilePage/ProfilePage.duck.js new file mode 100644 index 00000000..b2045057 --- /dev/null +++ b/src/containers/ProfilePage/ProfilePage.duck.js @@ -0,0 +1,64 @@ +import { addMarketplaceEntities } from '../../ducks/marketplaceData.duck'; +import { fetchCurrentUser } from '../../ducks/user.duck'; +import { storableError } from '../../util/errors'; + +// ================ Action types ================ // + +export const SHOW_USER_REQUEST = 'app/ProfilePage/SHOW_USER_REQUEST'; +export const SHOW_USER_SUCCESS = 'app/ProfilePage/SHOW_USER_SUCCESS'; +export const SHOW_USER_ERROR = 'app/ProfilePage/SHOW_USER_ERROR'; + +// ================ Reducer ================ // + +const initialState = { + userId: null, + userShowInProgress: false, + userShowError: null, +}; + +export default function profilePageReducer(state = initialState, action = {}) { + const { type, payload } = action; + switch (type) { + case SHOW_USER_REQUEST: + return { ...state, userShowInProgress: true, userShowError: null, userId: payload.userId }; + case SHOW_USER_SUCCESS: + return { ...state, userShowInProgress: false }; + case SHOW_USER_ERROR: + return { ...state, userShowInProgress: false, userShowError: payload }; + + default: + return state; + } +} + +// ================ Action creators ================ // + +export const showUserRequest = userId => ({ + type: SHOW_USER_REQUEST, + payload: { userId }, +}); + +export const showUserSuccess = () => ({ + type: SHOW_USER_SUCCESS, +}); + +export const showUserError = e => ({ + type: SHOW_USER_ERROR, + error: true, + payload: e, +}); + +// ================ Thunks ================ // + +export const showUser = userId => (dispatch, getState, sdk) => { + dispatch(showUserRequest(userId)); + dispatch(fetchCurrentUser()); + return sdk.users + .show({ id: userId, include: ['profileImage'] }) + .then(response => { + dispatch(addMarketplaceEntities(response)); + dispatch(showUserSuccess()); + return response; + }) + .catch(e => showUserError(storableError(e))); +}; diff --git a/src/containers/ProfilePage/ProfilePage.js b/src/containers/ProfilePage/ProfilePage.js index 2bbdd0d2..e4228e92 100644 --- a/src/containers/ProfilePage/ProfilePage.js +++ b/src/containers/ProfilePage/ProfilePage.js @@ -1,47 +1,170 @@ import React from 'react'; import PropTypes from 'prop-types'; +import { FormattedMessage } from 'react-intl'; import { compose } from 'redux'; import { connect } from 'react-redux'; +import classNames from 'classnames'; +import { types } from '../../util/sdkLoader'; import * as propTypes from '../../util/propTypes'; +import { ensureCurrentUser, ensureUser } from '../../util/data'; import { isScrollingDisabled } from '../../ducks/UI.duck'; -import { Page } from '../../components'; +import { getMarketplaceEntities } from '../../ducks/marketplaceData.duck'; +import { + Page, + LayoutSideNavigation, + LayoutWrapperMain, + LayoutWrapperSideNav, + LayoutWrapperTopbar, + LayoutWrapperFooter, + Footer, + AvatarLarge, + NamedLink, +} from '../../components'; import { TopbarContainer } from '../../containers'; +import { showUser } from './ProfilePage.duck'; + +import css from './ProfilePage.css'; + +const { UUID } = types; export const ProfilePageComponent = props => { - const { logoutError, params, scrollingDisabled } = props; + const { + logoutError, + scrollingDisabled, + currentUser, + user, + userShowInProgress, + userShowError, + } = props; + const ensuredCurrentUser = ensureCurrentUser(currentUser); + const profileUser = ensureUser(user); + const isCurrentUser = + ensuredCurrentUser.id && profileUser.id && ensuredCurrentUser.id.uuid === profileUser.id.uuid; + const displayName = profileUser.attributes.profile.displayName; + const bio = profileUser.attributes.profile.bio; + const listings = []; + const hasBio = !!bio; + const hasListings = listings.length > 0; + const editLinkMobile = isCurrentUser ? ( + + + + ) : null; + const editLinkDesktop = isCurrentUser ? ( + + + + ) : null; + + const asideContent = ( +
+ +

+ {displayName ? ( + + ) : null} +

+ {editLinkMobile} + {editLinkDesktop} +
+ ); + + const listingsContainerClasses = classNames(css.listingsContainer, { + [css.withBioMissingAbove]: !hasBio, + }); + + const mainContent = ( +
+

+ +

+ {hasBio ?

{bio}

: null} + {hasListings ? ( +
+

+ +

+
+ ) : null} +
+ ); + + let content; + + if (userShowError) { + content = ( +

+ +

+ ); + } else if (userShowInProgress) { + content = ( +

+ +

+ ); + } else { + content = mainContent; + } + + const schemaTitle = 'Profile page'; return ( - - + + + + + + {asideContent} + {content} + +