diff --git a/src/components/TabNavHorizontal/TabNavHorizontal.example.js b/src/components/TabNavHorizontal/TabNavHorizontal.example.js
index 69024a1d..794c8dd4 100644
--- a/src/components/TabNavHorizontal/TabNavHorizontal.example.js
+++ b/src/components/TabNavHorizontal/TabNavHorizontal.example.js
@@ -1,5 +1,5 @@
import TabNavHorizontal from './TabNavHorizontal';
-import { TYPE_BUTTON, TYPE_LINK } from './TabNavHorizontal';
+import { TAB_TYPE_BUTTON, TAB_TYPE_LINK } from './TabNavHorizontal';
const selfLinkProps = {
name: 'StyleguideComponent',
@@ -13,7 +13,7 @@ export const LinkTabs = {
{ text: 'Normal', linkProps: selfLinkProps },
{ text: 'Selected', linkProps: selfLinkProps, selected: true },
],
- type: TYPE_LINK,
+ type: TAB_TYPE_LINK,
},
group: 'navigation',
};
@@ -24,7 +24,7 @@ export const ButtonTabs = {
component: TabNavHorizontal,
props: {
tabs: [{ text: 'Normal', onClick: noop }, { text: 'Selected', onClick: noop, selected: true }],
- type: TYPE_BUTTON,
+ type: TAB_TYPE_BUTTON,
},
group: 'navigation',
};
diff --git a/src/components/TabNavHorizontal/TabNavHorizontal.js b/src/components/TabNavHorizontal/TabNavHorizontal.js
index 5358ce5f..26e21dcd 100644
--- a/src/components/TabNavHorizontal/TabNavHorizontal.js
+++ b/src/components/TabNavHorizontal/TabNavHorizontal.js
@@ -21,8 +21,8 @@ import css from './TabNavHorizontal.css';
export const LIGHT_SKIN = 'light';
export const DARK_SKIN = 'dark';
-export const TYPE_LINK = 'link';
-export const TYPE_BUTTON = 'button';
+export const TAB_TYPE_LINK = 'link';
+export const TAB_TYPE_BUTTON = 'button';
const { arrayOf, bool, func, node, object, oneOf, string } = PropTypes;
@@ -105,7 +105,7 @@ LinkTab.propTypes = {
const TabNavHorizontal = props => {
const { className, rootClassName, tabRootClassName, tabs, skin, type } = props;
const isDark = skin === DARK_SKIN;
- const isButton = type === TYPE_BUTTON;
+ const isButton = type === TAB_TYPE_BUTTON;
const classes = classNames(rootClassName || css.root, { [css.darkSkin]: isDark }, className);
const tabClasses = tabRootClassName || css.tab;
return (
@@ -128,7 +128,7 @@ TabNavHorizontal.defaultProps = {
tabRootClassName: null,
tabClassName: null,
skin: LIGHT_SKIN,
- type: TYPE_LINK,
+ type: TAB_TYPE_LINK,
};
TabNavHorizontal.propTypes = {
@@ -137,7 +137,7 @@ TabNavHorizontal.propTypes = {
tabRootClassName: string,
tabs: arrayOf(object).isRequired,
skin: oneOf([LIGHT_SKIN, DARK_SKIN]),
- type: oneOf([TYPE_LINK, TYPE_BUTTON]),
+ type: oneOf([TAB_TYPE_LINK, TAB_TYPE_BUTTON]),
};
export default TabNavHorizontal;
diff --git a/src/containers/ProfilePage/ProfilePage.css b/src/containers/ProfilePage/ProfilePage.css
index 920c7910..6aad5e17 100644
--- a/src/containers/ProfilePage/ProfilePage.css
+++ b/src/containers/ProfilePage/ProfilePage.css
@@ -99,7 +99,14 @@
}
.listingsContainer {
+ @apply --clearfix;
border-top: 1px solid var(--matterColorNegative);
+ border-bottom: 1px solid var(--matterColorNegative);
+ padding-bottom: 23px;
+
+ @media (--viewportMedium) {
+ padding-bottom: 58px;
+ }
}
.listingsTitle {
@@ -158,3 +165,33 @@
}
}
}
+
+.mobileReviews {
+ @media (--viewportMedium) {
+ display: none;
+ }
+}
+
+.mobileReviewsTitle {
+ @apply --marketplaceH3FontStyles;
+ color: var(--matterColorAnti);
+ margin: 24px 0;
+}
+
+.desktopReviews {
+ display: none;
+
+ @media (--viewportMedium) {
+ display: block;
+ margin-top: 28px;
+ }
+}
+
+.desktopReviewsTitle {
+ @apply --marketplaceH3FontStyles;
+ margin-bottom: 0;
+}
+
+.desktopReviewsTabNav {
+ margin-bottom: 32px;
+}
diff --git a/src/containers/ProfilePage/ProfilePage.js b/src/containers/ProfilePage/ProfilePage.js
index e33200ea..0fc932a2 100644
--- a/src/containers/ProfilePage/ProfilePage.js
+++ b/src/containers/ProfilePage/ProfilePage.js
@@ -1,4 +1,4 @@
-import React from 'react';
+import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { injectIntl, intlShape, FormattedMessage } from 'react-intl';
import { compose } from 'redux';
@@ -20,7 +20,10 @@ import {
AvatarLarge,
NamedLink,
ListingCard,
+ Reviews,
+ TabNavHorizontal,
} from '../../components';
+import { TAB_TYPE_BUTTON } from '../../components/TabNavHorizontal/TabNavHorizontal';
import { TopbarContainer } from '../../containers';
import { loadData } from './ProfilePage.duck';
import config from '../../config';
@@ -29,127 +32,241 @@ import css from './ProfilePage.css';
const { UUID } = types;
-export const ProfilePageComponent = props => {
- const {
- scrollingDisabled,
- currentUser,
- user,
- userShowError,
- queryListingsError,
- listings,
- intl,
- } = 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 hasBio = !!bio;
- const hasListings = listings.length > 0;
+export class ProfilePageComponent extends Component {
+ constructor(props) {
+ super(props);
- const editLinkMobile = isCurrentUser ? (
-
-
-
- ) : null;
- const editLinkDesktop = isCurrentUser ? (
-
-
-
- ) : null;
+ this.state = {
+ reviews: propTypes.REVIEW_TYPE_OF_PROVIDER,
+ };
- const asideContent = (
-
-
-
- {displayName ? (
-
- ) : null}
-
- {editLinkMobile}
- {editLinkDesktop}
-
- );
-
- const listingsContainerClasses = classNames(css.listingsContainer, {
- [css.withBioMissingAbove]: !hasBio,
- });
-
- const mainContent = (
-
-
-
-
- {hasBio ?
{bio}
: null}
- {hasListings ? (
-
-
-
-
-
- {listings.map(l => (
- -
-
-
- ))}
-
-
- ) : null}
-
- );
-
- let content;
-
- if (userShowError || queryListingsError) {
- content = (
-
-
-
- );
- } else {
- content = mainContent;
+ this.showOfProviderReviews = this.showOfProviderReviews.bind(this);
+ this.showOfCustomerReviews = this.showOfCustomerReviews.bind(this);
}
- const schemaTitle = intl.formatMessage(
- {
- id: 'ProfilePage.schemaTitle',
- },
- {
- name: displayName,
- siteTitle: config.siteTitle,
- }
- );
+ showOfProviderReviews() {
+ this.setState({
+ reviews: propTypes.REVIEW_TYPE_OF_PROVIDER,
+ });
+ }
- return (
-
-
-
-
-
- {asideContent}
- {content}
-
-
-
-
-
- );
-};
+ showOfCustomerReviews() {
+ this.setState({
+ reviews: propTypes.REVIEW_TYPE_OF_CUSTOMER,
+ });
+ }
+
+ render() {
+ const {
+ scrollingDisabled,
+ currentUser,
+ user,
+ userShowError,
+ queryListingsError,
+ listings,
+ reviews,
+ queryReviewsError,
+ intl,
+ } = this.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 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 reviewsError = (
+
+
+
+ );
+
+ const reviewsOfProvider = reviews.filter(
+ r => r.attributes.type === propTypes.REVIEW_TYPE_OF_PROVIDER
+ );
+
+ const reviewsOfCustomer = reviews.filter(
+ r => r.attributes.type === propTypes.REVIEW_TYPE_OF_CUSTOMER
+ );
+
+ const mobileReviews = (
+
+
+
+
+ {queryReviewsError ? reviewsError : null}
+
+
+
+
+ {queryReviewsError ? reviewsError : null}
+
+
+ );
+
+ const desktopReviewTabs = [
+ {
+ text: (
+
+
+
+ ),
+ selected: this.state.reviews === propTypes.REVIEW_TYPE_OF_PROVIDER,
+ onClick: this.showOfProviderReviews,
+ },
+ {
+ text: (
+
+
+
+ ),
+ selected: this.state.reviews === propTypes.REVIEW_TYPE_OF_CUSTOMER,
+ onClick: this.showOfCustomerReviews,
+ },
+ ];
+
+ const desktopReviews = (
+
+
+
+ {queryReviewsError ? reviewsError : null}
+
+ {this.state.reviews === propTypes.REVIEW_TYPE_OF_PROVIDER ? (
+
+ ) : (
+
+ )}
+
+ );
+
+ const mainContent = (
+
+
+
+
+ {hasBio ?
{bio}
: null}
+ {hasListings ? (
+
+
+
+
+
+ {listings.map(l => (
+ -
+
+
+ ))}
+
+
+ ) : null}
+ {mobileReviews}
+ {desktopReviews}
+
+ );
+
+ let content;
+
+ if (userShowError || queryListingsError) {
+ content = (
+
+
+
+ );
+ } else {
+ content = mainContent;
+ }
+
+ const schemaTitle = intl.formatMessage(
+ {
+ id: 'ProfilePage.schemaTitle',
+ },
+ {
+ name: displayName,
+ siteTitle: config.siteTitle,
+ }
+ );
+
+ return (
+
+
+
+
+
+ {asideContent}
+ {content}
+
+
+
+
+
+ );
+ }
+}
ProfilePageComponent.defaultProps = {
currentUser: null,
user: null,
userShowError: null,
queryListingsError: null,
+ reviews: [],
+ queryReviewsError: null,
};
const { bool, arrayOf } = PropTypes;
@@ -161,6 +278,8 @@ ProfilePageComponent.propTypes = {
userShowError: propTypes.error,
queryListingsError: propTypes.error,
listings: arrayOf(propTypes.listing).isRequired,
+ reviews: arrayOf(propTypes.review),
+ queryReviewsError: propTypes.error,
// from injectIntl
intl: intlShape.isRequired,
@@ -168,7 +287,14 @@ ProfilePageComponent.propTypes = {
const mapStateToProps = state => {
const { currentUser } = state.user;
- const { userId, userShowError, queryListingsError, userListingRefs } = state.ProfilePage;
+ const {
+ userId,
+ userShowError,
+ queryListingsError,
+ userListingRefs,
+ reviews,
+ queryReviewsError,
+ } = state.ProfilePage;
const userMatches = getMarketplaceEntities(state, [{ type: 'user', id: userId }]);
const user = userMatches.length === 1 ? userMatches[0] : null;
const listings = getMarketplaceEntities(state, userListingRefs);
@@ -179,6 +305,8 @@ const mapStateToProps = state => {
userShowError,
queryListingsError,
listings,
+ reviews,
+ queryReviewsError,
};
};
diff --git a/src/containers/ProfilePage/__snapshots__/ProfilePage.test.js.snap b/src/containers/ProfilePage/__snapshots__/ProfilePage.test.js.snap
index 68c8f215..18cc17f5 100644
--- a/src/containers/ProfilePage/__snapshots__/ProfilePage.test.js.snap
+++ b/src/containers/ProfilePage/__snapshots__/ProfilePage.test.js.snap
@@ -76,6 +76,79 @@ exports[`ProfilePage matches snapshot 1`] = `
}
/>
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ,
+ },
+ Object {
+ "onClick": [Function],
+ "selected": false,
+ "text":
+
+
,
+ },
+ ]
+ }
+ type="button"
+ />
+
+
{
// Possible amount of stars in a review
export const REVIEW_RATINGS = [1, 2, 3, 4, 5];
+// Review types: review of a provider and of a customer
+export const REVIEW_TYPE_OF_PROVIDER = 'ofProvider';
+export const REVIEW_TYPE_OF_CUSTOMER = 'ofCustomer';
+
// A review on a user
export const review = shape({
id: uuid.isRequired,
@@ -269,7 +273,7 @@ export const review = shape({
content: string,
rating: oneOf(REVIEW_RATINGS),
state: string.isRequired,
- type: string.isRequired,
+ type: oneOf([REVIEW_TYPE_OF_PROVIDER, REVIEW_TYPE_OF_CUSTOMER]).isRequired,
}).isRequired,
author: user,
subject: user,