Merge pull request #401 from sharetribe/handle-banned-users

Handle banned users
This commit is contained in:
Kimmo Puputti 2017-09-15 16:20:11 +03:00 committed by GitHub
commit 85fa852869
23 changed files with 468 additions and 74 deletions

View file

@ -80,3 +80,8 @@
font-weight: var(--fontWeightSemiBold);
padding-bottom: 6px;
}
.bannedUserIcon {
width: 100%;
height: 100%;
}

View file

@ -0,0 +1,171 @@
import Avatar, { AvatarMedium, AvatarLarge } from './Avatar';
import { types } from '../../util/sdkLoader';
import { fakeIntl } from '../../util/test-data';
const { UUID } = types;
const bannedUser = {
id: new UUID('banned-user'),
type: 'user',
attributes: {
banned: true,
},
};
const userWithoutProfileImage = {
id: new UUID('user-without-profile-image'),
type: 'user',
attributes: {
profile: {
displayName: 'No Profile',
abbreviatedName: 'NP',
},
},
};
const userWithProfileImage = {
id: new UUID('user-with-profile-image'),
type: 'user',
attributes: {
profile: {
displayName: 'Has Profile',
abbreviatedName: 'HP',
},
},
profileImage: {
id: new UUID('profile-image'),
type: 'image',
attributes: {
sizes: [
{
name: 'square-xlarge2x',
width: 240,
height: 240,
url: 'https://lorempixel.com/240/240/people/',
},
{
name: 'square-xlarge4x',
width: 480,
height: 480,
url: 'https://lorempixel.com/480/480/people/',
},
],
},
},
};
// ================ Empty user ================ //
export const EmptyUser = {
component: Avatar,
props: {
user: null,
intl: fakeIntl,
},
group: 'avatar',
};
export const EmptyUserMedium = {
component: AvatarMedium,
props: {
user: null,
intl: fakeIntl,
},
group: 'avatar',
};
export const EmptyUserLarge = {
component: AvatarLarge,
props: {
user: null,
intl: fakeIntl,
},
group: 'avatar',
};
// ================ Banned user ================ //
export const BannedUser = {
component: Avatar,
props: {
user: bannedUser,
intl: fakeIntl,
},
group: 'avatar',
};
export const BannedUserMedium = {
component: AvatarMedium,
props: {
user: bannedUser,
intl: fakeIntl,
},
group: 'avatar',
};
export const BannedUserLarge = {
component: AvatarLarge,
props: {
user: bannedUser,
intl: fakeIntl,
},
group: 'avatar',
};
// ================ No profile image ================ //
export const WithoutProfileImageUser = {
component: Avatar,
props: {
user: userWithoutProfileImage,
intl: fakeIntl,
},
group: 'avatar',
};
export const WithoutProfileImageUserMedium = {
component: AvatarMedium,
props: {
user: userWithoutProfileImage,
intl: fakeIntl,
},
group: 'avatar',
};
export const WithoutProfileImageUserLarge = {
component: AvatarLarge,
props: {
user: userWithoutProfileImage,
intl: fakeIntl,
},
group: 'avatar',
};
// ================ Full user with profile image ================ //
export const WithProfileImageUser = {
component: Avatar,
props: {
user: userWithProfileImage,
intl: fakeIntl,
},
group: 'avatar',
};
export const WithProfileImageUserMedium = {
component: AvatarMedium,
props: {
user: userWithProfileImage,
intl: fakeIntl,
},
group: 'avatar',
};
export const WithProfileImageUserLarge = {
component: AvatarLarge,
props: {
user: userWithProfileImage,
intl: fakeIntl,
},
group: 'avatar',
};

View file

@ -1,16 +1,25 @@
import React, { PropTypes } from 'react';
import { injectIntl, intlShape } from 'react-intl';
import classNames from 'classnames';
import * as propTypes from '../../util/propTypes';
import { ensureUser } from '../../util/data';
import { ResponsiveImage } from '../../components/';
import { ensureUser, userDisplayName, userAbbreviatedName } from '../../util/data';
import { ResponsiveImage, IconBannedUser } from '../../components/';
import css from './Avatar.css';
const Avatar = props => {
const { rootClassName, className, user } = props;
export const AvatarComponent = props => {
const { rootClassName, className, user, intl } = props;
const classes = classNames(rootClassName || css.root, className);
const avatarUser = ensureUser(user);
const { displayName, abbreviatedName } = avatarUser.attributes.profile;
const isBannedUser = avatarUser.attributes.banned;
const bannedUserDisplayName = intl.formatMessage({
id: 'Avatar.bannedUserDisplayName',
});
const bannedUserAbbreviatedName = '';
const displayName = userDisplayName(avatarUser, bannedUserDisplayName);
const abbreviatedName = userAbbreviatedName(avatarUser, bannedUserAbbreviatedName);
if (avatarUser.profileImage && avatarUser.profileImage.id) {
return (
@ -26,6 +35,12 @@ const Avatar = props => {
/>
</div>
);
} else if (isBannedUser) {
return (
<div className={classes} title={displayName}>
<IconBannedUser className={css.bannedUserIcon} />
</div>
);
} else {
// Placeholder avatar (initials)
return (
@ -38,18 +53,23 @@ const Avatar = props => {
const { string, oneOfType } = PropTypes;
Avatar.defaultProps = {
AvatarComponent.defaultProps = {
className: null,
rootClassName: null,
user: null,
};
Avatar.propTypes = {
AvatarComponent.propTypes = {
rootClassName: string,
className: string,
user: oneOfType([propTypes.user, propTypes.currentUser]),
// from injectIntl
intl: intlShape.isRequired,
};
const Avatar = injectIntl(AvatarComponent);
export default Avatar;
export const AvatarMedium = props => <Avatar rootClassName={css.mediumAvatar} {...props} />;

View file

@ -0,0 +1,13 @@
@import '../../marketplace.css';
.backgroundFill {
fill: var(--failColor);
}
.foregroundFill {
fill: var(--matterColorLight);
}
.foregroundStroke {
stroke: var(--matterColorLight);
}

View file

@ -0,0 +1,30 @@
import React, { PropTypes } from 'react';
import css from './IconBannedUser.css';
const IconBannedUser = props => {
const { className } = props;
return (
<svg
className={className}
width="40"
height="40"
viewBox="0 0 40 40"
xmlns="http://www.w3.org/2000/svg"
>
<g fill="none" fillRule="evenodd">
<circle className={css.backgroundFill} cx="20" cy="20" r="20" />
<circle className={css.foregroundStroke} strokeWidth="3" cx="20" cy="20" r="13" />
<path className={css.foregroundFill} d="M28.34 9.04l2.12 2.12-19.8 19.8-2.12-2.12z" />
</g>
</svg>
);
};
IconBannedUser.defaultProps = { className: null };
const { string } = PropTypes;
IconBannedUser.propTypes = { className: string };
export default IconBannedUser;

View file

@ -76,7 +76,7 @@
}
}
.title {
.heading {
margin: 28px 24px 0 24px;
@media (--viewportMedium) {

View file

@ -1,9 +1,9 @@
import React, { PropTypes } from 'react';
import { FormattedDate, FormattedMessage } from 'react-intl';
import { injectIntl, intlShape, FormattedDate, FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import * as propTypes from '../../util/propTypes';
import { createSlug } from '../../util/urlHelpers';
import { ensureListing, ensureTransaction, ensureUser } from '../../util/data';
import { ensureListing, ensureTransaction, ensureUser, userDisplayName } from '../../util/data';
import { BookingBreakdown, NamedLink, ResponsiveImage, AvatarMedium } from '../../components';
import css from './OrderDetailsPanel.css';
@ -110,27 +110,43 @@ const orderMessage = (
}
};
const OrderDetailsPanel = props => {
export const OrderDetailsPanelComponent = props => {
const {
rootClassName,
className,
transaction,
intl,
} = props;
const currentTransaction = ensureTransaction(transaction);
const currentListing = ensureListing(currentTransaction.listing);
const currentProvider = ensureUser(currentTransaction.provider);
const currentCustomer = ensureUser(currentTransaction.customer);
const providerProfile = currentProvider.attributes.profile;
const authorDisplayName = providerProfile.displayName;
const customerDisplayName = currentCustomer.attributes.profile.displayName;
const listingLoaded = !!currentListing.id;
const listingDeleted = listingLoaded && currentListing.attributes.deleted;
const bannedUserDisplayName = intl.formatMessage({
id: 'OrderDetailsPanel.bannedUserDisplayName',
});
const deletedListingTitle = intl.formatMessage({
id: 'OrderDetailsPanel.deletedListingTitle',
});
const deletedListingOrderTitle = intl.formatMessage({
id: 'OrderDetailsPanel.deletedListingOrderTitle',
});
const orderMessageDeletedListing = intl.formatMessage({
id: 'OrderDetailsPanel.messageDeletedListing',
});
const authorDisplayName = userDisplayName(currentProvider, bannedUserDisplayName);
const customerDisplayName = userDisplayName(currentCustomer, bannedUserDisplayName);
const transactionState = currentTransaction.attributes.state;
const lastTransitionedAt = currentTransaction.attributes.lastTransitionedAt;
const lastTransition = currentTransaction.attributes.lastTransitione;
let listingLink = null;
if (currentListing.id && currentListing.attributes.title) {
if (listingLoaded && currentListing.attributes.title) {
const title = currentListing.attributes.title;
const params = { id: currentListing.id.uuid, slug: createSlug(title) };
listingLink = (
@ -138,19 +154,30 @@ const OrderDetailsPanel = props => {
{title}
</NamedLink>
);
} else {
listingLink = deletedListingOrderTitle;
}
const listingTitle = currentListing.attributes.title;
const listingTitle = currentListing.attributes.deleted
? deletedListingTitle
: currentListing.attributes.title;
const bookingInfo = breakdown(currentTransaction);
const title = orderTitle(transactionState, listingLink, customerDisplayName, lastTransition);
const message = orderMessage(
const orderHeading = orderTitle(
transactionState,
listingLink,
authorDisplayName,
lastTransitionedAt,
customerDisplayName,
lastTransition
);
const message = listingDeleted
? orderMessageDeletedListing
: orderMessage(
transactionState,
listingLink,
authorDisplayName,
lastTransitionedAt,
lastTransition
);
const firstImage = currentListing.images && currentListing.images.length > 0
? currentListing.images[0]
@ -177,7 +204,7 @@ const OrderDetailsPanel = props => {
<AvatarMedium user={currentProvider} />
</div>
<div className={css.orderInfo}>
<h1 className={css.title}>{title}</h1>
<h1 className={css.heading}>{orderHeading}</h1>
<div className={css.message}>
{message}
</div>
@ -225,7 +252,7 @@ const OrderDetailsPanel = props => {
);
};
OrderDetailsPanel.defaultProps = {
OrderDetailsPanelComponent.defaultProps = {
rootClassName: null,
className: null,
lastTransition: null,
@ -233,10 +260,15 @@ OrderDetailsPanel.defaultProps = {
const { string } = PropTypes;
OrderDetailsPanel.propTypes = {
OrderDetailsPanelComponent.propTypes = {
rootClassName: string,
className: string,
transaction: propTypes.transaction.isRequired,
// from injectIntl
intl: intlShape,
};
const OrderDetailsPanel = injectIntl(OrderDetailsPanelComponent);
export default OrderDetailsPanel;

View file

@ -3,8 +3,9 @@ import { shallow } from 'enzyme';
import { types } from '../../util/sdkLoader';
import { createBooking, createListing, createUser, createTransaction } from '../../util/test-data';
import { renderShallow } from '../../util/test-helpers';
import { fakeIntl } from '../../util/test-data';
import { BookingBreakdown } from '../../components';
import OrderDetailsPanel from './OrderDetailsPanel.js';
import { OrderDetailsPanelComponent } from './OrderDetailsPanel.js';
describe('OrderDetailsPanel', () => {
it('matches snapshot', () => {
@ -21,7 +22,7 @@ describe('OrderDetailsPanel', () => {
provider: createUser('provider'),
customer: createUser('customer'),
});
const tree = renderShallow(<OrderDetailsPanel transaction={tx} />);
const tree = renderShallow(<OrderDetailsPanelComponent transaction={tx} intl={fakeIntl} />);
expect(tree).toMatchSnapshot();
});
it('renders correct total price', () => {
@ -38,7 +39,7 @@ describe('OrderDetailsPanel', () => {
provider: createUser('provider'),
customer: createUser('customer'),
});
const panel = shallow(<OrderDetailsPanel transaction={tx} />);
const panel = shallow(<OrderDetailsPanelComponent transaction={tx} intl={fakeIntl} />);
const breakdownProps = panel.find(BookingBreakdown).props();
expect(breakdownProps.transaction.attributes.payinTotal).toEqual(new Money(16500, 'USD'));
});

View file

@ -1,9 +1,9 @@
import React, { PropTypes } from 'react';
import { FormattedDate, FormattedMessage } from 'react-intl';
import { injectIntl, intlShape, FormattedDate, FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import * as propTypes from '../../util/propTypes';
import { createSlug } from '../../util/urlHelpers';
import { ensureListing, ensureTransaction, ensureUser } from '../../util/data';
import { ensureListing, ensureTransaction, ensureUser, userDisplayName } from '../../util/data';
import {
AvatarLarge,
AvatarMedium,
@ -99,7 +99,7 @@ const saleMessage = (saleState, customerName, lastTransitionedAt, lastTransition
}
};
const SaleDetailsPanel = props => {
export const SaleDetailsPanelComponent = props => {
const {
rootClassName,
className,
@ -107,12 +107,19 @@ const SaleDetailsPanel = props => {
onAcceptSale,
onRejectSale,
acceptOrRejectInProgress,
intl,
} = props;
const currentTransaction = ensureTransaction(transaction);
const currentListing = ensureListing(currentTransaction.listing);
const currentCustomer = ensureUser(currentTransaction.customer);
const customerLoaded = !!currentCustomer.id;
const isCustomerBanned = customerLoaded && currentCustomer.attributes.banned;
const customerDisplayName = currentCustomer.attributes.profile.displayName;
const bannedUserDisplayName = intl.formatMessage({
id: 'SaleDetailsPanel.bannedUserDisplayName',
});
const customerDisplayName = userDisplayName(currentCustomer, bannedUserDisplayName);
const transactionState = currentTransaction.attributes.state;
const lastTransitionedAt = currentTransaction.attributes.lastTransitionedAt;
const lastTransition = currentTransaction.attributes.lastTransition;
@ -132,12 +139,11 @@ const SaleDetailsPanel = props => {
const bookingInfo = breakdown(currentTransaction);
const title = saleTitle(transactionState, listingLink, customerDisplayName, lastTransition);
const message = saleMessage(
transactionState,
customerDisplayName,
lastTransitionedAt,
lastTransition
);
const message = isCustomerBanned
? intl.formatMessage({
id: 'SaleDetailsPanel.customerBannedStatus',
})
: saleMessage(transactionState, customerDisplayName, lastTransitionedAt, lastTransition);
const listingTitle = currentListing.attributes.title;
const firstImage = currentListing.images && currentListing.images.length > 0
@ -146,7 +152,8 @@ const SaleDetailsPanel = props => {
const isPreauthorizedState = currentTransaction.attributes.state ===
propTypes.TX_STATE_PREAUTHORIZED;
const actionButtons = isPreauthorizedState
const canShowButtons = isPreauthorizedState && !isCustomerBanned;
const actionButtons = canShowButtons
? <div className={css.actionButtons}>
<SecondaryButton
className={css.rejectButton}
@ -224,7 +231,7 @@ const SaleDetailsPanel = props => {
);
};
SaleDetailsPanel.defaultProps = {
SaleDetailsPanelComponent.defaultProps = {
rootClassName: null,
className: null,
lastTransition: null,
@ -232,13 +239,18 @@ SaleDetailsPanel.defaultProps = {
const { string, func, bool } = PropTypes;
SaleDetailsPanel.propTypes = {
SaleDetailsPanelComponent.propTypes = {
rootClassName: string,
className: string,
transaction: propTypes.transaction.isRequired,
onAcceptSale: func.isRequired,
onRejectSale: func.isRequired,
acceptOrRejectInProgress: bool.isRequired,
// from injectIntl
intl: intlShape.isRequired,
};
const SaleDetailsPanel = injectIntl(SaleDetailsPanelComponent);
export default SaleDetailsPanel;

View file

@ -3,8 +3,9 @@ import { shallow } from 'enzyme';
import { types } from '../../util/sdkLoader';
import { createTransaction, createBooking, createListing, createUser } from '../../util/test-data';
import { renderShallow } from '../../util/test-helpers';
import { fakeIntl } from '../../util/test-data';
import { BookingBreakdown } from '../../components';
import SaleDetailsPanel from './SaleDetailsPanel.js';
import { SaleDetailsPanelComponent } from './SaleDetailsPanel.js';
const noop = () => null;
@ -29,8 +30,9 @@ describe('SaleDetailsPanel', () => {
onAcceptSale: noop,
onRejectSale: noop,
acceptOrRejectInProgress: false,
intl: fakeIntl,
};
const tree = renderShallow(<SaleDetailsPanel {...props} />);
const tree = renderShallow(<SaleDetailsPanelComponent {...props} />);
expect(tree).toMatchSnapshot();
});
it('renders correct total price', () => {
@ -53,8 +55,9 @@ describe('SaleDetailsPanel', () => {
onAcceptSale: noop,
onRejectSale: noop,
acceptOrRejectInProgress: false,
intl: fakeIntl,
};
const panel = shallow(<SaleDetailsPanel {...props} />);
const panel = shallow(<SaleDetailsPanelComponent {...props} />);
const breakdownProps = panel.find(BookingBreakdown).props();
// Total price for the provider should be transaction total minus the commission.

View file

@ -17,6 +17,7 @@ import ExternalLink from './ExternalLink/ExternalLink';
import ExpandingTextarea from './ExpandingTextarea/ExpandingTextarea';
import FilterPanel from './FilterPanel/FilterPanel';
import HeroSection from './HeroSection/HeroSection';
import IconBannedUser from './IconBannedUser/IconBannedUser';
import IconEmailAttention from './IconEmailAttention/IconEmailAttention';
import IconEmailSent from './IconEmailSent/IconEmailSent';
import IconEmailSuccess from './IconEmailSuccess/IconEmailSuccess';
@ -90,6 +91,7 @@ export {
ExternalLink,
FilterPanel,
HeroSection,
IconBannedUser,
IconEmailAttention,
IconEmailSent,
IconEmailSuccess,

View file

@ -175,10 +175,6 @@
border-bottom: none;
}
&:hover .itemUsername {
color: var(--marketplaceColor);
}
@media (--viewportLarge) {
margin-bottom: 21px;
padding-bottom: 18px;
@ -194,6 +190,11 @@
&:hover {
text-decoration: none;
}
&:hover .itemUsername {
/* Hightlight the username when the whole item is hovered */
color: var(--marketplaceColor);
}
}
.itemAvatar {
@ -327,6 +328,10 @@
margin-bottom: 1px;
}
.stateName {
/* This class is empty on purpose, it is used below for banned users */
}
.stateRequested,
.statePending {
font-weight: var(--fontWeightSemiBold);
@ -376,3 +381,13 @@
.lastTransitionedAtDelivered {
color: var(--matterColorAnti);
}
.bannedUserLink {
& .itemUsername,
&:hover .itemUsername,
& .bookingInfo,
& .stateName,
& .lastTransitionedAt {
color: var(--matterColorAnti);
}
}

View file

@ -16,6 +16,7 @@ import {
} from '../../components';
import * as propTypes from '../../util/propTypes';
import { formatMoney } from '../../util/currency';
import { userDisplayName } from '../../util/data';
import { getMarketplaceEntities } from '../../ducks/marketplaceData.duck';
import { sendVerificationEmail } from '../../ducks/user.duck';
import { logout, authenticationInProgress } from '../../ducks/Auth.duck';
@ -93,7 +94,11 @@ export const InboxItem = props => {
const isOrder = type === 'order';
const otherUser = isOrder ? provider : customer;
const otherUserDisplayName = otherUser.attributes.profile.displayName;
const bannedUserDisplayName = intl.formatMessage({
id: 'InboxPage.bannedUserDisplayName',
});
const isOtherUserBanned = otherUser.attributes.banned;
const otherUserDisplayName = userDisplayName(otherUser, bannedUserDisplayName);
const stateData = txState(intl, tx, isOrder);
const isSaleNotification = !isOrder && tx.attributes.state === propTypes.TX_STATE_PREAUTHORIZED;
@ -104,9 +109,13 @@ export const InboxItem = props => {
const bookingPrice = isOrder ? tx.attributes.payinTotal : tx.attributes.payoutTotal;
const price = formatMoney(intl, bookingPrice);
const linkClasses = classNames(css.itemLink, {
[css.bannedUserLink]: isOtherUserBanned,
});
return (
<NamedLink
className={css.itemLink}
className={linkClasses}
name={isOrder ? 'OrderDetailsPage' : 'SaleDetailsPage'}
params={{ id: tx.id.uuid }}
>
@ -126,7 +135,7 @@ export const InboxItem = props => {
</div>
</div>
<div className={css.itemState}>
<div className={stateData.stateClassName}>{stateData.state}</div>
<div className={classNames(css.stateName, stateData.stateClassName)}>{stateData.state}</div>
<div
className={classNames(css.lastTransitionedAt, stateData.lastTransitionedAtClassName)}
title={lastTransitionedAt.long}

View file

@ -294,7 +294,7 @@ exports[`InboxPage matches snapshot 2`] = `
<div
className={undefined}>
<div
className={undefined}>
className="">
InboxPage.stateRequested
</div>
<div
@ -605,7 +605,7 @@ exports[`InboxPage matches snapshot 4`] = `
<div
className={undefined}>
<div
className={undefined}>
className="">
InboxPage.statePending
</div>
<div

View file

@ -52,7 +52,9 @@ describe('ListingPage', () => {
return showListing(id)(dispatch, null, sdk).then(data => {
expect(data).toEqual(response);
expect(show.mock.calls).toEqual([[{ id, include: ['author', 'author.profileImage', 'images'] }]]);
expect(show.mock.calls).toEqual([
[{ id, include: ['author', 'author.profileImage', 'images'] }],
]);
expect(dispatch.mock.calls).toEqual([
[showListingRequest(id)],
[expect.anything()], // fetchCurrentUser() call
@ -76,7 +78,9 @@ describe('ListingPage', () => {
},
e => {
expect(e).toEqual(error);
expect(show.mock.calls).toEqual([[{ id, include: ['author', 'author.profileImage', 'images'] }]]);
expect(show.mock.calls).toEqual([
[{ id, include: ['author', 'author.profileImage', 'images'] }],
]);
expect(dispatch.mock.calls).toEqual([
[showListingRequest(id)],
[expect.anything()], // fetchCurrentUser() call

View file

@ -1,5 +1,6 @@
import { types } from '../../util/sdkLoader';
import { addMarketplaceEntities } from '../../ducks/marketplaceData.duck';
import { updatedEntities, denormalisedEntities } from '../../util/data';
// ================ Action types ================ //
@ -56,11 +57,20 @@ export const fetchOrder = id =>
.then(response => {
txResponse = response;
const listingId = listingRelationship(response).id;
const entities = updatedEntities({}, response.data);
const denormalised = denormalisedEntities(entities, 'listing', [listingId]);
const listing = denormalised[0];
return sdk.listings.show({
id: listingId,
include: ['author', 'author.profileImage', 'images'],
});
const canFetchListing = listing && listing.attributes && !listing.attributes.deleted;
if (canFetchListing) {
return sdk.listings.show({
id: listingId,
include: ['author', 'author.profileImage', 'images'],
});
} else {
return response;
}
})
.then(response => {
dispatch(addMarketplaceEntities(txResponse));

View file

@ -43,10 +43,8 @@ exports[`OrderPage matches snapshot 1`] = `
onResendVerificationEmail={[Function]}
sendVerificationEmailError={null}
sendVerificationEmailInProgress={false} />
<OrderDetailsPanel
<InjectIntl(Component)
className="undefined"
lastTransition={null}
rootClassName={null}
transaction={
Object {
"attributes": Object {

View file

@ -7,7 +7,11 @@ import * as propTypes from '../../util/propTypes';
import { sendVerificationEmail } from '../../ducks/user.duck';
import { logout, authenticationInProgress } from '../../ducks/Auth.duck';
import { manageDisableScrolling, isScrollingDisabled } from '../../ducks/UI.duck';
import { recoverPassword, retypePasswordRecoveryEmail, clearPasswordRecoveryError } from './PasswordRecoveryPage.duck';
import {
recoverPassword,
retypePasswordRecoveryEmail,
clearPasswordRecoveryError,
} from './PasswordRecoveryPage.duck';
import { PageLayout, Topbar, InlineTextButton, KeysIcon } from '../../components';
import { PasswordRecoveryForm } from '../../containers';

View file

@ -44,13 +44,11 @@ exports[`SalePage matches snapshot 1`] = `
sendVerificationEmailError={null}
sendVerificationEmailInProgress={false} />
<div>
<SaleDetailsPanel
<InjectIntl(Component)
acceptOrRejectInProgress={false}
className="undefined"
lastTransition={null}
onAcceptSale={[Function]}
onRejectSale={[Function]}
rootClassName={null}
transaction={
Object {
"attributes": Object {

View file

@ -1,5 +1,6 @@
// components
import * as AddImages from './components/AddImages/AddImages.example';
import * as Avatar from './components/Avatar/Avatar.example';
import * as BirthdayInputField from './components/BirthdayInputField/BirthdayInputField.example';
import * as BookingBreakdown from './components/BookingBreakdown/BookingBreakdown.example';
import * as Button from './components/Button/Button.example';
@ -52,6 +53,7 @@ import * as Typography from './containers/StyleguidePage/Typography.example';
export {
AddImages,
Avatar,
BirthdayInputField,
BookingBreakdown,
BookingDatesForm,

View file

@ -17,6 +17,7 @@
"AuthenticationPage.verifyEmailClose": "LATER",
"AuthenticationPage.verifyEmailText": "Thanks for signing up! There's one quick step left. To be able to contact you, we need you to verify your email address. Please click the link we sent to {email}.",
"AuthenticationPage.verifyEmailTitle": "{name}, check your inbox to verify your email",
"Avatar.bannedUserDisplayName": "Banned user",
"BookingBreakdown.bookingPeriod": "{bookingStart} {bookingEnd}",
"BookingBreakdown.commission": "Saunatime fee",
"BookingBreakdown.nightCount": "{count, number} {count, plural, one {night} other {nights}}",
@ -130,6 +131,7 @@
"HeroSection.title": "Book saunas everywhere.",
"ImageCarousel.imageAltText": "Image {index}/{count}",
"ImageFromFile.couldNotReadFile": "Could not read file",
"InboxPage.bannedUserDisplayName": "Banned user",
"InboxPage.fetchFailed": "Could not load all messages. Please try again.",
"InboxPage.noOrdersFound": "You haven't made any bookings.",
"InboxPage.noSalesFound": "Nobody has booked anything from you yet.",
@ -191,8 +193,12 @@
"MapPriceMarker.unsupportedPrice": "({currency})",
"Modal.close": "CLOSE",
"Modal.closeModal": "Close modal",
"OrderDetailsPanel.bannedUserDisplayName": "Banned user",
"OrderDetailsPanel.bookingBreakdownTitle": "Booking breakdown",
"OrderDetailsPanel.deletedListingOrderTitle": "a listing",
"OrderDetailsPanel.deletedListingTitle": "Deleted listing",
"OrderDetailsPanel.hostedBy": "Hosted by {name}",
"OrderDetailsPanel.messageDeletedListing": "However, the listing is deleted and cannot be viewed anymore.",
"OrderDetailsPanel.orderAcceptedStatus": "{providerName} accepted the request on {transitionDate}.",
"OrderDetailsPanel.orderAcceptedSubtitle": "You have booked {listingLink}",
"OrderDetailsPanel.orderAcceptedTitle": "Woohoo {customerName}!",
@ -312,7 +318,9 @@
"ProfileSettingsForm.yourProfilePicture": "Your profile picture",
"ProfileSettingsPage.title": "Profile settings",
"ResponsiveImage.noImage": "No image",
"SaleDetailsPanel.bannedUserDisplayName": "Banned user",
"SaleDetailsPanel.bookingBreakdownTitle": "Booking breakdown",
"SaleDetailsPanel.customerBannedStatus": "The user made the request, but was later banned. You cannot accept or reject the request.",
"SaleDetailsPanel.listingAcceptedTitle": "Woohoo! You accepted a request from {customerName} for {listingLink}",
"SaleDetailsPanel.listingDeliveredTitle": "{customerName} booked {listingLink}",
"SaleDetailsPanel.listingRejectedTitle": "{customerName} requested to book {listingLink}",

View file

@ -163,3 +163,54 @@ export const ensureCurrentUser = user => {
const empty = { id: null, type: 'current-user', attributes: { profile: {} }, profileImage: {} };
return { ...empty, ...user };
};
/**
* Get the display name of the given user. This function handles
* missing data (e.g. when the user object is still being downloaded),
* fully loaded users, as well as banned users.
*
* For banned users, a translated name should be provided.
*
* @param {propTypes.user} user
* @param {String} bannedUserDisplayName
*
* @return {String} display name that can be rendered in the UI
*/
export const userDisplayName = (user, bannedUserDisplayName) => {
const hasAttributes = user && user.attributes;
const hasProfile = hasAttributes && user.attributes.profile;
if (hasAttributes && user.attributes.banned) {
return bannedUserDisplayName;
} else if (hasProfile) {
return user.attributes.profile.displayName;
} else {
return '';
}
};
/**
* Get the abbreviated name of the given user. This function handles
* missing data (e.g. when the user object is still being downloaded),
* fully loaded users, as well as banned users.
*
* For banned users, a translated name should be provided.
*
* @param {propTypes.user} user
* @param {String} bannedUserAbbreviatedName
*
* @return {String} abbreviated name that can be rendered in the UI
* (e.g. in Avatar initials)
*/
export const userAbbreviatedName = (user, bannedUserAbbreviatedName) => {
const hasAttributes = user && user.attributes;
const hasProfile = hasAttributes && user.attributes.profile;
if (hasAttributes && user.attributes.banned) {
return bannedUserAbbreviatedName;
} else if (hasProfile) {
return user.attributes.profile.abbreviatedName;
} else {
return '';
}
};

View file

@ -22,7 +22,7 @@ import Decimal from 'decimal.js';
import { types as sdkTypes } from './sdkLoader';
const { UUID, LatLng, LatLngBounds, Money } = sdkTypes;
const { arrayOf, bool, func, instanceOf, number, oneOf, shape, string } = PropTypes;
const { arrayOf, bool, func, instanceOf, number, oneOf, oneOfType, shape, string } = PropTypes;
// Fixed value
export const value = val => oneOf([val]);
@ -85,7 +85,7 @@ export const user = shape({
profile: shape({
displayName: string.isRequired,
abbreviatedName: string.isRequired,
}).isRequired,
}),
}),
});
@ -105,18 +105,24 @@ export const image = shape({
}),
});
const listingAttributes = shape({
title: string.isRequired,
description: string.isRequired,
address: string.isRequired,
geolocation: latlng.isRequired,
open: bool.isRequired,
price: money,
});
const deletedListingAttributes = shape({
deleted: value(true).isRequired,
});
// Denormalised listing object
export const listing = shape({
id: uuid.isRequired,
type: value('listing').isRequired,
attributes: shape({
title: string.isRequired,
description: string.isRequired,
address: string.isRequired,
geolocation: latlng.isRequired,
open: bool.isRequired,
price: money,
}),
attributes: oneOfType([listingAttributes, deletedListingAttributes]).isRequired,
author: user,
images: arrayOf(image),
});