Split TransactionPanel to dummy subcomponents

and reduce direct state check agains current transaction.
This commit is contained in:
Vesa Luusua 2019-01-22 20:10:19 +02:00
parent 538faf945d
commit 32fc6ba5b1
12 changed files with 1337 additions and 2438 deletions

View file

@ -66,7 +66,7 @@
font-weight: var(--fontWeightSemiBold);
/* Move so that baseline aligns with the total price */
padding-top: 7px;
padding-top: 6px;
@media (--viewportMedium) {
padding-top: 8px;
@ -77,9 +77,11 @@
@apply --marketplaceBodyFontStyles;
font-weight: var(--fontWeightSemiBold);
margin: 0 0 0 10px;
padding-top: 4px;
@media (--viewportMedium) {
margin: 2px 0 0 10px;
padding-top: 0;
}
}

View file

@ -0,0 +1,29 @@
import React from 'react';
import classNames from 'classnames';
import { ExternalLink } from '../../components';
import css from './TransactionPanel.css';
// Functional component as a helper to build AddressLinkMaybe
const AddressLinkMaybe = props => {
const { className, rootClassName, location, geolocation, showAddress } = props;
const { address, building } = location || {};
const { lat, lng } = geolocation || {};
const hrefToGoogleMaps = geolocation
? `https://maps.google.com/?q=${lat},${lng}`
: address
? `https://maps.google.com/?q=${encodeURIComponent(address)}`
: null;
const fullAddress =
typeof building === 'string' && building.length > 0 ? `${building}, ${address}` : address;
const classes = classNames(rootClassName || css.address, className);
return showAddress && hrefToGoogleMaps ? (
<p className={classes}>
<ExternalLink href={hrefToGoogleMaps}>{fullAddress}</ExternalLink>
</p>
) : null;
};
export default AddressLinkMaybe;

View file

@ -0,0 +1,33 @@
import React from 'react';
import { FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import config from '../../config';
import { BookingBreakdown } from '../../components';
import css from './TransactionPanel.css';
// Functional component as a helper to build BookingBreakdown
const BreakdownMaybe = props => {
const { className, rootClassName, breakdownClassName, transaction, transactionRole } = props;
const loaded = transaction && transaction.id && transaction.booking && transaction.booking.id;
const classes = classNames(rootClassName || css.breakdownMaybe, className);
const breakdownClasses = classNames(breakdownClassName || css.breakdown);
return loaded ? (
<div className={classes}>
<h3 className={css.bookingBreakdownTitle}>
<FormattedMessage id="TransactionPanel.bookingBreakdownTitle" />
</h3>
<BookingBreakdown
className={breakdownClasses}
userRole={transactionRole}
unitType={config.bookingUnitType}
transaction={transaction}
booking={transaction.booking}
/>
</div>
) : null;
};
export default BreakdownMaybe;

View file

@ -0,0 +1,26 @@
import React from 'react';
import AddressLinkMaybe from './AddressLinkMaybe';
import css from './TransactionPanel.css';
// Functional component as a helper to build detail card headings
const DetailCardHeadingsMaybe = props => {
const {
showDetailCardHeadings,
listingTitle,
subTitle,
location,
geolocation,
showAddress,
} = props;
return showDetailCardHeadings ? (
<div className={css.detailCardHeadings}>
<h2 className={css.detailCardTitle}>{listingTitle}</h2>
<p className={css.detailCardSubtitle}>{subTitle}</p>
<AddressLinkMaybe location={location} geolocation={geolocation} showAddress={showAddress} />
</div>
) : null;
};
export default DetailCardHeadingsMaybe;

View file

@ -0,0 +1,40 @@
import React from 'react';
import classNames from 'classnames';
import { AvatarMedium, ResponsiveImage } from '../../components';
import css from './TransactionPanel.css';
// Functional component as a helper to build AddressLinkMaybe
const DetailCardImage = props => {
const {
className,
rootClassName,
avatarWrapperClassName,
listingTitle,
image,
provider,
isCustomer,
} = props;
const classes = classNames(rootClassName || css.detailCardImageWrapper, className);
return (
<React.Fragment>
<div className={classes}>
<div className={css.aspectWrapper}>
<ResponsiveImage
rootClassName={css.rootForImage}
alt={listingTitle}
image={image}
variants={['landscape-crop', 'landscape-crop2x']}
/>
</div>
</div>
{isCustomer ? (
<div className={avatarWrapperClassName || css.avatarWrapper}>
<AvatarMedium user={provider} />
</div>
) : null}
</React.Fragment>
);
};
export default DetailCardImage;

View file

@ -0,0 +1,64 @@
import React from 'react';
import { FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import { ActivityFeed } from '../../components';
import css from './TransactionPanel.css';
// Functional component as a helper to build ActivityFeed section
const FeedSection = props => {
const {
className,
rootClassName,
currentTransaction,
currentUser,
fetchMessagesError,
fetchMessagesInProgress,
initialMessageFailed,
messages,
oldestMessagePageFetched,
onShowMoreMessages,
onOpenReviewModal,
totalMessagePages,
} = props;
const txTransitions = currentTransaction.attributes.transitions
? currentTransaction.attributes.transitions
: [];
const hasOlderMessages = totalMessagePages > oldestMessagePageFetched;
const showFeed =
messages.length > 0 || txTransitions.length > 0 || initialMessageFailed || fetchMessagesError;
const classes = classNames(rootClassName || css.feedContainer, className);
return showFeed ? (
<div className={classes}>
<h3 className={css.feedHeading}>
<FormattedMessage id="TransactionPanel.activityHeading" />
</h3>
{initialMessageFailed ? (
<p className={css.messageError}>
<FormattedMessage id="TransactionPanel.initialMessageFailed" />
</p>
) : null}
{fetchMessagesError ? (
<p className={css.messageError}>
<FormattedMessage id="TransactionPanel.messageLoadingFailed" />
</p>
) : null}
<ActivityFeed
className={css.feed}
messages={messages}
transaction={currentTransaction}
currentUser={currentUser}
hasOlderMessages={hasOlderMessages && !fetchMessagesInProgress}
onOpenReviewModal={onOpenReviewModal}
onShowOlderMessages={onShowMoreMessages}
fetchMessagesInProgress={fetchMessagesInProgress}
/>
</div>
) : null;
};
export default FeedSection;

View file

@ -0,0 +1,226 @@
import React from 'react';
import { FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import { createSlug, stringify } from '../../util/urlHelpers';
import { NamedLink } from '../../components';
import css from './TransactionPanel.css';
export const HEADING_ENQUIRED = 'enquired';
export const HEADING_REQUESTED = 'requested';
export const HEADING_ACCEPTED = 'accepted';
export const HEADING_DECLINED = 'declined';
export const HEADING_CANCELED = 'canceled';
export const HEADING_DELIVERED = 'deliveded';
const createListingLink = (listingId, label, listingDeleted, searchParams = {}, className = '') => {
if (!listingDeleted) {
const params = { id: listingId, slug: createSlug(label) };
const to = { search: stringify(searchParams) };
return (
<NamedLink className={className} name="ListingPage" params={params} to={to}>
{label}
</NamedLink>
);
} else {
return <FormattedMessage id="TransactionPanel.deletedListingOrderTitle" />;
}
};
const ListingDeletedInfoMaybe = props => {
return props.listingDeleted ? (
<p className={css.transactionInfoMessage}>
<FormattedMessage id="TransactionPanel.messageDeletedListing" />
</p>
) : null;
};
const HeadingCustomer = props => {
const { className, id, values, listingDeleted } = props;
return (
<React.Fragment>
<h1 className={className}>
<span className={css.mainTitle}>
<FormattedMessage id={id} values={values} />
</span>
</h1>
<ListingDeletedInfoMaybe listingDeleted={listingDeleted} />
</React.Fragment>
);
};
const HeadingCustomerWithSubtitle = props => {
const { className, id, values, subtitleId, subtitleValues, children, listingDeleted } = props;
return (
<React.Fragment>
<h1 className={className}>
<span className={css.mainTitle}>
<FormattedMessage id={id} values={values} />
</span>
<FormattedMessage id={subtitleId} values={subtitleValues} />
</h1>
{children}
<ListingDeletedInfoMaybe listingDeleted={listingDeleted} />
</React.Fragment>
);
};
const CustomerBannedInfoMaybe = props => {
return props.isCustomerBanned ? (
<p className={css.transactionInfoMessage}>
<FormattedMessage id="TransactionPanel.customerBannedStatus" />
</p>
) : null;
};
const HeadingProvider = props => {
const { className, id, values, isCustomerBanned } = props;
return (
<React.Fragment>
<h1 className={className}>
<span className={css.mainTitle}>
<FormattedMessage id={id} values={values} />
</span>
</h1>
<CustomerBannedInfoMaybe isCustomerBanned={isCustomerBanned} />
</React.Fragment>
);
};
// Functional component as a helper to choose and show Order or Sale heading info:
// title, subtitle, and message
const PanelHeading = props => {
const {
className,
rootClassName,
panelHeadingState,
customerName,
providerName,
listingId,
listingTitle,
listingDeleted,
isCustomerBanned,
} = props;
const isCustomer = props.transactionRole === 'customer';
const defaultRootClassName = isCustomer ? css.headingOrder : css.headingSale;
const titleClasses = classNames(rootClassName || defaultRootClassName, className);
const listingLink = createListingLink(listingId, listingTitle, listingDeleted);
switch (panelHeadingState) {
case HEADING_ENQUIRED:
return isCustomer ? (
<HeadingCustomer
className={titleClasses}
id="TransactionPanel.orderEnquiredTitle"
values={{ listingLink }}
listingDeleted={listingDeleted}
/>
) : (
<HeadingProvider
className={titleClasses}
id="TransactionPanel.saleEnquiredTitle"
values={{ customerName, listingLink }}
isCustomerBanned={isCustomerBanned}
/>
);
case HEADING_REQUESTED:
return isCustomer ? (
<HeadingCustomerWithSubtitle
className={titleClasses}
id="TransactionPanel.orderPreauthorizedTitle"
values={{ customerName }}
subtitleId="TransactionPanel.orderPreauthorizedSubtitle"
subtitleValues={{ listingLink }}
>
{!listingDeleted ? (
<p className={css.transactionInfoMessage}>
<FormattedMessage
id="TransactionPanel.orderPreauthorizedInfo"
values={{ providerName }}
/>
</p>
) : null}
</HeadingCustomerWithSubtitle>
) : (
<HeadingProvider
className={titleClasses}
id="TransactionPanel.saleRequestedTitle"
values={{ customerName, listingLink }}
>
{!isCustomerBanned ? (
<p className={titleClasses}>
<FormattedMessage id="TransactionPanel.saleRequestedInfo" values={{ customerName }} />
</p>
) : null}
</HeadingProvider>
);
case HEADING_ACCEPTED:
return isCustomer ? (
<HeadingCustomerWithSubtitle
className={titleClasses}
id="TransactionPanel.orderPreauthorizedTitle"
values={{ customerName }}
subtitleId="TransactionPanel.orderAcceptedSubtitle"
subtitleValues={{ listingLink }}
/>
) : (
<HeadingProvider
className={titleClasses}
id="TransactionPanel.saleAcceptedTitle"
values={{ customerName, listingLink }}
/>
);
case HEADING_DECLINED:
return isCustomer ? (
<HeadingCustomer
className={titleClasses}
id="TransactionPanel.orderDeclinedTitle"
values={{ customerName, listingLink }}
/>
) : (
<HeadingProvider
className={titleClasses}
id="TransactionPanel.saleDeclinedTitle"
values={{ customerName, listingLink }}
isCustomerBanned={isCustomerBanned}
/>
);
case HEADING_CANCELED:
return isCustomer ? (
<HeadingCustomer
className={titleClasses}
id="TransactionPanel.orderCancelledTitle"
values={{ customerName, listingLink }}
/>
) : (
<HeadingProvider
className={titleClasses}
id="TransactionPanel.saleCancelledTitle"
values={{ customerName, listingLink }}
/>
);
case HEADING_DELIVERED:
return isCustomer ? (
<HeadingCustomer
className={titleClasses}
id="TransactionPanel.orderDeliveredTitle"
values={{ customerName, listingLink }}
isCustomerBanned={isCustomerBanned}
/>
) : (
<HeadingProvider
className={titleClasses}
id="TransactionPanel.saleDeliveredTitle"
values={{ customerName, listingLink }}
isCustomerBanned={isCustomerBanned}
/>
);
default:
console.warning('Unknown state given to panel heading.');
return null;
}
};
export default PanelHeading;

View file

@ -0,0 +1,64 @@
import React from 'react';
import { FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import { PrimaryButton, SecondaryButton } from '../../components';
import css from './TransactionPanel.css';
// Functional component as a helper to build ActionButtons for
// provider when state is preauthorized
const SaleActionButtonsMaybe = props => {
const {
className,
rootClassName,
showButtons,
acceptInProgress,
declineInProgress,
acceptSaleError,
declineSaleError,
onAcceptSale,
onDeclineSale,
} = props;
const buttonsDisabled = acceptInProgress || declineInProgress;
const acceptErrorMessage = acceptSaleError ? (
<p className={css.actionError}>
<FormattedMessage id="TransactionPanel.acceptSaleFailed" />
</p>
) : null;
const declineErrorMessage = declineSaleError ? (
<p className={css.actionError}>
<FormattedMessage id="TransactionPanel.declineSaleFailed" />
</p>
) : null;
const classes = classNames(rootClassName || css.actionButtons, className);
return showButtons ? (
<div className={classes}>
<div className={css.actionErrors}>
{acceptErrorMessage}
{declineErrorMessage}
</div>
<div className={css.actionButtonWrapper}>
<SecondaryButton
inProgress={declineInProgress}
disabled={buttonsDisabled}
onClick={onDeclineSale}
>
<FormattedMessage id="TransactionPanel.declineButton" />
</SecondaryButton>
<PrimaryButton
inProgress={acceptInProgress}
disabled={buttonsDisabled}
onClick={onAcceptSale}
>
<FormattedMessage id="TransactionPanel.acceptButton" />
</PrimaryButton>
</div>
</div>
) : null;
};
export default SaleActionButtonsMaybe;

View file

@ -29,14 +29,17 @@
}
}
.imageWrapperMobile {
/* DetailCardImage subcomponent */
.detailCardImageWrapper {
/* Layout */
display: block;
display: none;
width: 100%;
position: relative;
@media (--viewportLarge) {
display: none;
display: block;
max-height: 268px;
overflow-y: hidden;
}
}
@ -60,37 +63,51 @@
}
}
.avatarWrapperMobile {
.avatarWrapper {
/* Position (over the listing image)*/
margin-left: 24px;
margin-top: -31px;
margin-top: -30px;
/* Rendering context to the same lavel as listing image */
position: relative;
/* Layout */
display: block;
}
.avatarWrapperCustomerDesktop {
display: none;
composes: avatarWrapperMobile;
@media (--viewportLarge) {
display: block;
margin-left: 48px;
}
}
.avatarMobile {
/* Flex would give too much width by default. */
width: 60px;
@media (--viewportMedium) {
margin-top: -32px;
padding: 2px 0;
}
@media (--viewportLarge) {
margin-left: 48px;
width: unset;
padding: 2px 0;
}
}
/* Passed-in props for DetailCardImage subcomponent */
.imageWrapperMobile {
@media (--viewportLarge) {
display: none;
}
}
.avatarWrapperMobile {
composes: avatarWrapper;
@media (--viewportLarge) {
display: none;
}
}
.avatarWrapperDesktop {
composes: avatarWrapper;
display: none;
@media (--viewportLarge) {
display: block;
}
}
.avatarWrapperProviderDesktop {
display: none;
@ -105,30 +122,37 @@
}
}
/* PanelHeadings subcomponent */
.headingOrder {
margin: 29px 24px 0 24px;
@media (--viewportMedium) {
margin: 25px 24px 0 24px;
max-width: 80%;
margin: 24px 24px 0 24px;
padding: 1px 0 7px 0;
}
@media (--viewportLarge) {
max-width: 100%;
margin: 152px 0 0 0;
padding: 0;
}
}
.headingSale {
margin: 22px 24px 0 24px;
margin: 18px 24px 0 24px;
padding: 5px 0 1px 0;
@media (--viewportMedium) {
max-width: 80%;
margin: 24px 24px 0 24px;
padding: 1px 0 7px 0;
}
@media (--viewportLarge) {
max-width: 100%;
margin: 42px 0 0 0;
padding: 0;
}
}
@ -147,28 +171,30 @@
}
}
.transitionDate {
white-space: nowrap;
}
/* Container for booking details in mobile layout */
.bookingDetailsMobile {
margin-top: 47px;
@media (--viewportMedium) {
margin-top: 43px;
margin-top: 40px;
padding: 4px 0 0px 0;
}
@media (--viewportLarge) {
display: none;
}
}
/* "aside" section in desktop layout */
.asideDesktop {
margin: 1px 0 0 0;
/**
* Aside is visible on mobile layout too, since it includes BookingPanel component.
* It might get rendered as a Modal in mobile layout.
*/
@media (--viewportLarge) {
margin-top: 123px;
margin-left: 0;
margin-right: 0;
margin: 123px 0 0 0;
}
}
@ -184,26 +210,13 @@
}
}
.detailCardImageWrapper {
display: none;
/* Layout */
width: 100%;
position: relative;
@media (--viewportLarge) {
display: block;
}
}
/* DetailCardHeadingsMaybe subcomponent */
.detailCardHeadings {
display: none;
@media (--viewportLarge) {
display: block;
margin-left: 48px;
margin-right: 48px;
margin-bottom: 37px;
margin: 0 48px 33px 48px;
}
}
@ -228,16 +241,25 @@
}
}
/* AddressLinkMaybe subcomponent */
.address {
@apply --marketplaceH5FontStyles;
color: var(--matterColorAnti);
margin: 0;
}
.addressMobileWrapper {
/* Passed-in props for AddressLinkMaybe subcomponent */
.addressMobile {
@apply --marketplaceH5FontStyles;
color: var(--matterColorAnti);
margin: 0 24px 24px 24px;
}
/* BreakdownMaybe subcomponent */
.breakdownMaybe {
/* default "root" class for breakdown container */
}
.bookingBreakdownTitle {
/* Font */
color: var(--matterColorAnti);
@ -246,14 +268,8 @@
@media (--viewportLarge) {
margin: 37px 48px 26px 48px;
}
}
.breakdownContainer {
display: none;
@media (--viewportLarge) {
display: block;
margin: 32px 48px 24px 48px;
padding: 4px 0 4px 0;
}
}
@ -264,27 +280,12 @@
margin: 18px 24px 0 24px;
}
@media (--viewportLarge) {
margin: 14px 48px 47px 48px;
}
}
.feedContainer {
margin: 46px 24px 0 24px;
@media (--viewportMedium) {
margin: 46px 24px 0 24px;
}
@media (--viewportLarge) {
margin: 43px 0 0 0;
}
}
.feedContainerWithInfoAbove {
@media (--viewportLarge) {
margin: 38px 0 0 0;
margin: 24px 48px 47px 48px;
padding: 6px 0 2px 0;
}
}
/* FeedSection subcomponent */
.feedHeading {
color: var(--matterColorAnti);
margin: 0;
@ -298,6 +299,32 @@
margin-top: 20px;
}
.messageError {
color: var(--failColor);
margin: 13px 0 22px 0;
@media (--viewportMedium) {
margin: 13px 0 23px 0;
}
@media (--viewportLarge) {
margin: 12px 0 23px 0;
}
}
/* Passed-in props for FeedSection subcomponent */
.feedContainer {
margin: 46px 24px 0 24px;
@media (--viewportMedium) {
margin: 40px 24px 0 24px;
}
@media (--viewportLarge) {
margin: 43px 0 0 0;
}
}
/* Prop to be passed to SendMessageForm component */
.sendMessageForm {
position: relative;
margin: 46px 24px 0 24px;
@ -311,12 +338,7 @@
}
}
.requestToBookButton {
@apply --marketplaceButtonStylesPrimary;
text-decoration: none;
}
/* SaleActionButtonsMaybe subcomponent */
.actionButtons {
/* Position action button row above the footer */
z-index: 9;
@ -342,23 +364,6 @@
}
}
.mobileActionButtons {
display: block;
@media (--viewportLarge) {
display: none;
}
}
.desktopActionButtons {
display: none;
@media (--viewportLarge) {
display: block;
margin-bottom: 48px;
}
}
.actionButtonWrapper {
width: 100%;
display: flex;
@ -380,19 +385,6 @@
}
}
.messageError {
color: var(--failColor);
margin: 13px 0 22px 0;
@media (--viewportMedium) {
margin: 13px 0 23px 0;
}
@media (--viewportLarge) {
margin: 12px 0 23px 0;
}
}
.actionError {
@apply --marketplaceH5FontStyles;
color: var(--failColor);
@ -416,6 +408,25 @@
}
}
/* Container for SaleActionButtonsMaybe subcomponent */
.mobileActionButtons {
display: block;
@media (--viewportLarge) {
display: none;
}
}
.desktopActionButtons {
display: none;
@media (--viewportLarge) {
display: block;
margin-bottom: 48px;
}
}
/* BookingPanel subcompnent */
.bookingPanel {
margin: 16px 48px 48px 48px;
}

View file

@ -1,550 +0,0 @@
import React from 'react';
import { FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import {
txHasBeenAccepted,
txHasFirstReview,
txIsAccepted,
txIsCanceled,
txIsCompleted,
txIsDeclined,
txIsEnquired,
txIsExpired,
txIsRequested,
txIsReviewed,
} from '../../util/types';
import { userDisplayName } from '../../util/data';
import { createSlug, stringify } from '../../util/urlHelpers';
import {
ActivityFeed,
BookingBreakdown,
BookingPanel,
ExternalLink,
NamedLink,
PrimaryButton,
SecondaryButton,
} from '../../components';
import config from '../../config';
import css from './TransactionPanel.css';
// Functional component as a helper to build ActivityFeed section
export const FeedSection = props => {
const {
className,
rootClassName,
currentTransaction,
currentUser,
fetchMessagesError,
fetchMessagesInProgress,
initialMessageFailed,
messages,
oldestMessagePageFetched,
onShowMoreMessages,
onOpenReviewModal,
totalMessagePages,
} = props;
const txTransitions = currentTransaction.attributes.transitions
? currentTransaction.attributes.transitions
: [];
const hasOlderMessages = totalMessagePages > oldestMessagePageFetched;
const showFeed =
messages.length > 0 || txTransitions.length > 0 || initialMessageFailed || fetchMessagesError;
const classes = classNames(rootClassName || css.feedContainer, className);
return showFeed ? (
<div className={classes}>
<h3 className={css.feedHeading}>
<FormattedMessage id="TransactionPanel.activityHeading" />
</h3>
{initialMessageFailed ? (
<p className={css.messageError}>
<FormattedMessage id="TransactionPanel.initialMessageFailed" />
</p>
) : null}
{fetchMessagesError ? (
<p className={css.messageError}>
<FormattedMessage id="TransactionPanel.messageLoadingFailed" />
</p>
) : null}
<ActivityFeed
className={css.feed}
messages={messages}
transaction={currentTransaction}
currentUser={currentUser}
hasOlderMessages={hasOlderMessages && !fetchMessagesInProgress}
onOpenReviewModal={onOpenReviewModal}
onShowOlderMessages={() => {
onShowMoreMessages(currentTransaction.id);
}}
fetchMessagesInProgress={fetchMessagesInProgress}
/>
</div>
) : null;
};
// Functional component as a helper to build AddressLinkMaybe
export const AddressLinkMaybe = props => {
const { transaction, transactionRole, currentListing } = props;
const isCustomer = transactionRole === 'customer';
const txIsAcceptedForCustomer = isCustomer && txHasBeenAccepted(transaction);
const publicData = currentListing.attributes.publicData || {};
const { address, building } = publicData.location || {};
const geolocation = currentListing.attributes.geolocation;
const { lat, lng } = geolocation || {};
const hrefToGoogleMaps = geolocation
? `https://maps.google.com/?q=${lat},${lng}`
: address
? `https://maps.google.com/?q=${encodeURIComponent(address)}`
: null;
const fullAddress =
typeof building === 'string' && building.length > 0 ? `${building}, ${address}` : address;
return txIsAcceptedForCustomer && hrefToGoogleMaps ? (
<p className={css.address}>
<ExternalLink href={hrefToGoogleMaps}>{fullAddress}</ExternalLink>
</p>
) : null;
};
// Functional component as a helper to build BookingBreakdown
export const BreakdownMaybe = props => {
const { className, rootClassName, breakdownClassName, transaction, transactionRole } = props;
const loaded = transaction && transaction.id && transaction.booking && transaction.booking.id;
const classes = classNames(rootClassName || className);
const breakdownClasses = classNames(css.breakdown, breakdownClassName);
return loaded ? (
<div className={classes}>
<h3 className={css.bookingBreakdownTitle}>
<FormattedMessage id="TransactionPanel.bookingBreakdownTitle" />
</h3>
<BookingBreakdown
className={breakdownClasses}
userRole={transactionRole}
unitType={config.bookingUnitType}
transaction={transaction}
booking={transaction.booking}
/>
</div>
) : null;
};
const createListingLink = (listing, label, searchParams = {}, className = '') => {
const listingLoaded = !!listing.id;
if (listingLoaded && !listing.attributes.deleted) {
const title = listing.attributes.title;
const params = { id: listing.id.uuid, slug: createSlug(title) };
const to = { search: stringify(searchParams) };
return (
<NamedLink className={className} name="ListingPage" params={params} to={to}>
{label}
</NamedLink>
);
} else {
return <FormattedMessage id="TransactionPanel.deletedListingOrderTitle" />;
}
};
// Functional component as a helper to build detail card headings
export const DetailCardHeadingsMaybe = props => {
const { transaction, transactionRole, listing, listingTitle, subTitle } = props;
const isCustomer = transactionRole === 'customer';
const canShowDetailCardHeadings = isCustomer && !txIsEnquired(transaction);
return canShowDetailCardHeadings ? (
<div className={css.detailCardHeadings}>
<h2 className={css.detailCardTitle}>{listingTitle}</h2>
<p className={css.detailCardSubtitle}>{subTitle}</p>
<AddressLinkMaybe
transaction={transaction}
transactionRole={transactionRole}
currentListing={listing}
/>
</div>
) : null;
};
// Functional component as a helper to build a BookingPanel
export const BookingPanelMaybe = props => {
const {
authorDisplayName,
transaction,
transactionRole,
listing,
listingTitle,
subTitle,
provider,
onSubmit,
onManageDisableScrolling,
timeSlots,
fetchTimeSlotsError,
} = props;
const isProviderLoaded = !!provider.id;
const isProviderBanned = isProviderLoaded && provider.attributes.banned;
const isCustomer = transactionRole === 'customer';
const canShowBookingPanel = isCustomer && txIsEnquired(transaction) && !isProviderBanned;
return canShowBookingPanel ? (
<BookingPanel
className={css.bookingPanel}
titleClassName={css.bookingTitle}
isOwnListing={false}
listing={listing}
handleBookingSubmit={() => console.log('submit')}
title={listingTitle}
subTitle={subTitle}
authorDisplayName={authorDisplayName}
onSubmit={onSubmit}
onManageDisableScrolling={onManageDisableScrolling}
timeSlots={timeSlots}
fetchTimeSlotsError={fetchTimeSlotsError}
/>
) : null;
};
// Functional component as a helper to build ActionButtons for
// provider when state is preauthorized
export const SaleActionButtonsMaybe = props => {
const {
className,
rootClassName,
canShowButtons,
transaction,
acceptInProgress,
declineInProgress,
acceptSaleError,
declineSaleError,
onAcceptSale,
onDeclineSale,
} = props;
const buttonsDisabled = acceptInProgress || declineInProgress;
const acceptErrorMessage = acceptSaleError ? (
<p className={css.actionError}>
<FormattedMessage id="TransactionPanel.acceptSaleFailed" />
</p>
) : null;
const declineErrorMessage = declineSaleError ? (
<p className={css.actionError}>
<FormattedMessage id="TransactionPanel.declineSaleFailed" />
</p>
) : null;
const classes = classNames(rootClassName || css.actionButtons, className);
return canShowButtons ? (
<div className={classes}>
<div className={css.actionErrors}>
{acceptErrorMessage}
{declineErrorMessage}
</div>
<div className={css.actionButtonWrapper}>
<SecondaryButton
inProgress={declineInProgress}
disabled={buttonsDisabled}
onClick={() => onDeclineSale(transaction.id)}
>
<FormattedMessage id="TransactionPanel.declineButton" />
</SecondaryButton>
<PrimaryButton
inProgress={acceptInProgress}
disabled={buttonsDisabled}
onClick={() => onAcceptSale(transaction.id)}
>
<FormattedMessage id="TransactionPanel.acceptButton" />
</PrimaryButton>
</div>
</div>
) : null;
};
// Functional component as a helper to build order title
export const OrderTitle = props => {
const {
className,
rootClassName,
transaction,
customerDisplayName: customerName,
currentListing,
listingTitle,
} = props;
const listingLink = createListingLink(currentListing, listingTitle);
const classes = classNames(rootClassName || css.headingOrder, className);
if (txIsEnquired(transaction)) {
return (
<h1 className={classes}>
<span className={css.mainTitle}>
<FormattedMessage id="TransactionPanel.orderEnquiredTitle" values={{ listingLink }} />
</span>
</h1>
);
} else if (txIsRequested(transaction)) {
return (
<h1 className={classes}>
<span className={css.mainTitle}>
<FormattedMessage
id="TransactionPanel.orderPreauthorizedTitle"
values={{ customerName }}
/>
</span>
<FormattedMessage
id="TransactionPanel.orderPreauthorizedSubtitle"
values={{ listingLink }}
/>
</h1>
);
} else if (txIsAccepted(transaction)) {
return (
<h1 className={classes}>
<span className={css.mainTitle}>
<FormattedMessage id="TransactionPanel.orderAcceptedTitle" values={{ customerName }} />
</span>
<FormattedMessage id="TransactionPanel.orderAcceptedSubtitle" values={{ listingLink }} />
</h1>
);
} else if (txIsDeclined(transaction)) {
return (
<h1 className={classes}>
<FormattedMessage
id="TransactionPanel.orderDeclinedTitle"
values={{ customerName, listingLink }}
/>
</h1>
);
} else if (txIsExpired(transaction)) {
return (
<h1 className={classes}>
<FormattedMessage
id="TransactionPanel.orderDeclinedTitle"
values={{ customerName, listingLink }}
/>
</h1>
);
} else if (txIsCanceled(transaction)) {
return (
<h1 className={classes}>
<FormattedMessage
id="TransactionPanel.orderCancelledTitle"
values={{ customerName, listingLink }}
/>
</h1>
);
} else if (
txIsCompleted(transaction) ||
txHasFirstReview(transaction) ||
txIsReviewed(transaction)
) {
return (
<h1 className={classes}>
<FormattedMessage
id="TransactionPanel.orderDeliveredTitle"
values={{ customerName, listingLink }}
/>
</h1>
);
} else {
return null;
}
};
// Functional component as a helper to build order message below title
export const OrderMessage = props => {
const {
className,
rootClassName,
transaction,
authorDisplayName: providerName,
listingDeleted,
} = props;
const classes = classNames(rootClassName || css.transactionInfoMessage, className);
if (!listingDeleted && txIsRequested(transaction)) {
return (
<p className={classes}>
<FormattedMessage id="TransactionPanel.orderPreauthorizedInfo" values={{ providerName }} />
</p>
);
} else if (listingDeleted) {
return (
<p className={classes}>
<FormattedMessage id="TransactionPanel.messageDeletedListing" />
</p>
);
}
return null;
};
// Functional component as a helper to build sale title
export const SaleTitle = props => {
const {
className,
rootClassName,
transaction,
customerDisplayName: customerName,
currentListing,
listingTitle,
} = props;
const listingLink = createListingLink(currentListing, listingTitle);
const classes = classNames(rootClassName || css.headingSale, className);
if (txIsEnquired(transaction)) {
return (
<h1 className={classes}>
<FormattedMessage
id="TransactionPanel.saleEnquiredTitle"
values={{ customerName, listingLink }}
/>
</h1>
);
} else if (txIsRequested(transaction)) {
return (
<h1 className={classes}>
<FormattedMessage
id="TransactionPanel.saleRequestedTitle"
values={{ customerName, listingLink }}
/>
</h1>
);
} else if (txIsAccepted(transaction)) {
return (
<h1 className={classes}>
<FormattedMessage
id="TransactionPanel.saleAcceptedTitle"
values={{ customerName, listingLink }}
/>
</h1>
);
} else if (txIsDeclined(transaction)) {
return (
<h1 className={classes}>
<FormattedMessage
id="TransactionPanel.saleDeclinedTitle"
values={{ customerName, listingLink }}
/>
</h1>
);
} else if (txIsExpired(transaction)) {
return (
<h1 className={classes}>
<FormattedMessage
id="TransactionPanel.saleDeclinedTitle"
values={{ customerName, listingLink }}
/>
</h1>
);
} else if (txIsCanceled(transaction)) {
return (
<h1 className={classes}>
<FormattedMessage
id="TransactionPanel.saleCancelledTitle"
values={{ customerName, listingLink }}
/>
</h1>
);
} else if (
txIsCompleted(transaction) ||
txHasFirstReview(transaction) ||
txIsReviewed(transaction)
) {
return (
<h1 className={classes}>
<FormattedMessage
id="TransactionPanel.saleDeliveredTitle"
values={{ customerName, listingLink }}
/>
</h1>
);
} else {
return null;
}
};
// Functional component as a helper to build sale message below title
export const SaleMessage = props => {
const {
className,
rootClassName,
transaction,
customerDisplayName: customerName,
isCustomerBanned,
} = props;
const classes = classNames(rootClassName || css.transactionInfoMessage, className);
if (!isCustomerBanned && txIsRequested(transaction)) {
return (
<p className={classes}>
<FormattedMessage id="TransactionPanel.saleRequestedInfo" values={{ customerName }} />
</p>
);
} else if (isCustomerBanned) {
return (
<p className={classes}>
<FormattedMessage id="TransactionPanel.customerBannedStatus" />
</p>
);
}
return null;
};
// Functional component as a helper to choose and show Order or Sale title
export const TransactionPageTitle = props => {
return props.transactionRole === 'customer' ? (
<OrderTitle {...props} />
) : (
<SaleTitle {...props} />
);
};
// Functional component as a helper to choose and show Order or Sale message
export const TransactionPageMessage = props => {
return props.transactionRole === 'customer' ? (
<OrderMessage {...props} />
) : (
<SaleMessage {...props} />
);
};
// Helper function to get display names for different roles
export const displayNames = (
currentUser,
currentProvider,
currentCustomer,
bannedUserDisplayName
) => {
const authorDisplayName = userDisplayName(currentProvider, bannedUserDisplayName);
const customerDisplayName = userDisplayName(currentCustomer, bannedUserDisplayName);
let otherUserDisplayName = '';
const currentUserIsCustomer =
currentUser.id && currentCustomer.id && currentUser.id.uuid === currentCustomer.id.uuid;
const currentUserIsProvider =
currentUser.id && currentProvider.id && currentUser.id.uuid === currentProvider.id.uuid;
if (currentUserIsCustomer) {
otherUserDisplayName = authorDisplayName;
} else if (currentUserIsProvider) {
otherUserDisplayName = customerDisplayName;
}
return {
authorDisplayName,
customerDisplayName,
otherUserDisplayName,
};
};

View file

@ -2,29 +2,64 @@ import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { injectIntl, intlShape } from 'react-intl';
import classNames from 'classnames';
import { txIsRequested, LINE_ITEM_NIGHT, LINE_ITEM_DAY, propTypes } from '../../util/types';
import { ensureListing, ensureTransaction, ensureUser } from '../../util/data';
import {
txIsAccepted,
txIsCanceled,
txIsDeclinedOrExpired,
txIsEnquired,
txIsRequested,
txHasBeenDelivered,
} from '../../util/transaction';
import { LINE_ITEM_NIGHT, LINE_ITEM_DAY, propTypes } from '../../util/types';
import { ensureListing, ensureTransaction, ensureUser, userDisplayName } from '../../util/data';
import { isMobileSafari } from '../../util/userAgent';
import { formatMoney } from '../../util/currency';
import { AvatarMedium, AvatarLarge, ResponsiveImage, ReviewModal } from '../../components';
import { AvatarLarge, BookingPanel, ReviewModal } from '../../components';
import { SendMessageForm } from '../../forms';
import config from '../../config';
// These are internal components that make this file more readable.
import {
AddressLinkMaybe,
BookingPanelMaybe,
BreakdownMaybe,
DetailCardHeadingsMaybe,
FeedSection,
SaleActionButtonsMaybe,
TransactionPageTitle,
TransactionPageMessage,
displayNames,
} from './TransactionPanel.helpers';
import AddressLinkMaybe from './AddressLinkMaybe';
import BreakdownMaybe from './BreakdownMaybe';
import DetailCardHeadingsMaybe from './DetailCardHeadingsMaybe';
import DetailCardImage from './DetailCardImage';
import FeedSection from './FeedSection';
import SaleActionButtonsMaybe from './SaleActionButtonsMaybe';
import PanelHeading, {
HEADING_ENQUIRED,
HEADING_REQUESTED,
HEADING_ACCEPTED,
HEADING_DECLINED,
HEADING_CANCELED,
HEADING_DELIVERED,
} from './PanelHeading';
import css from './TransactionPanel.css';
// Helper function to get display names for different roles
const displayNames = (currentUser, currentProvider, currentCustomer, bannedUserDisplayName) => {
const authorDisplayName = userDisplayName(currentProvider, bannedUserDisplayName);
const customerDisplayName = userDisplayName(currentCustomer, bannedUserDisplayName);
let otherUserDisplayName = '';
const currentUserIsCustomer =
currentUser.id && currentCustomer.id && currentUser.id.uuid === currentCustomer.id.uuid;
const currentUserIsProvider =
currentUser.id && currentProvider.id && currentUser.id.uuid === currentProvider.id.uuid;
if (currentUserIsCustomer) {
otherUserDisplayName = authorDisplayName;
} else if (currentUserIsProvider) {
otherUserDisplayName = customerDisplayName;
}
return {
authorDisplayName,
customerDisplayName,
otherUserDisplayName,
};
};
export class TransactionPanelComponent extends Component {
constructor(props) {
super(props);
@ -147,7 +182,48 @@ export class TransactionPanelComponent extends Component {
const listingDeleted = listingLoaded && currentListing.attributes.deleted;
const customerLoaded = !!currentCustomer.id;
const isCustomerBanned = customerLoaded && currentCustomer.attributes.banned;
const canShowSaleButtons = isProvider && txIsRequested(currentTransaction) && !isCustomerBanned;
const isProviderLoaded = !!currentProvider.id;
const isProviderBanned = isProviderLoaded && currentProvider.attributes.banned;
const stateDataFn = tx => {
if (txIsEnquired(tx)) {
return {
headingState: HEADING_ENQUIRED,
showBookingPanel: isCustomer && !isProviderBanned,
};
} else if (txIsRequested(tx)) {
return {
headingState: HEADING_REQUESTED,
showDetailCardHeadings: isCustomer,
showSaleButtons: isProvider && !isCustomerBanned,
};
} else if (txIsAccepted(tx)) {
return {
headingState: HEADING_ACCEPTED,
showDetailCardHeadings: isCustomer,
showAddress: isCustomer,
};
} else if (txIsDeclinedOrExpired(tx)) {
return {
headingState: HEADING_DECLINED,
showDetailCardHeadings: isCustomer,
};
} else if (txIsCanceled(tx)) {
return {
headingState: HEADING_CANCELED,
showDetailCardHeadings: isCustomer,
};
} else if (txHasBeenDelivered(tx)) {
return {
headingState: HEADING_DELIVERED,
showDetailCardHeadings: isCustomer,
showAddress: isCustomer,
};
} else {
return { headingState: 'unknown' };
}
};
const stateData = stateDataFn(currentTransaction);
const bannedUserDisplayName = intl.formatMessage({
id: 'TransactionPanel.bannedUserDisplayName',
@ -163,6 +239,8 @@ export class TransactionPanelComponent extends Component {
bannedUserDisplayName
);
const { publicData = {}, geolocation } = currentListing.attributes;
const location = publicData.location || {};
const listingTitle = currentListing.attributes.deleted
? deletedListingTitle
: currentListing.attributes.title;
@ -185,19 +263,15 @@ export class TransactionPanelComponent extends Component {
const firstImage =
currentListing.images && currentListing.images.length > 0 ? currentListing.images[0] : null;
const actionButtonClasses = classNames(css.actionButtons);
const saleButtons = (
<SaleActionButtonsMaybe
rootClassName={actionButtonClasses}
canShowButtons={canShowSaleButtons}
transaction={currentTransaction}
showButtons={stateData.showSaleButtons}
acceptInProgress={acceptInProgress}
declineInProgress={declineInProgress}
acceptSaleError={acceptSaleError}
declineSaleError={declineSaleError}
onAcceptSale={onAcceptSale}
onDeclineSale={onDeclineSale}
onAcceptSale={() => onAcceptSale(currentTransaction.id)}
onDeclineSale={() => onDeclineSale(currentTransaction.id)}
/>
);
@ -206,68 +280,49 @@ export class TransactionPanelComponent extends Component {
{ name: otherUserDisplayName }
);
const sendMessageFormClasses = classNames(css.sendMessageForm);
const showInfoMessage = listingDeleted || (!listingDeleted && txIsRequested(transaction)); // !!orderInfoMessage;
const feedContainerClasses = classNames(css.feedContainer, {
[css.feedContainerWithInfoAbove]: showInfoMessage,
});
const classes = classNames(rootClassName || css.root, className);
return (
<div className={classes}>
<div className={css.container}>
<div className={css.txInfo}>
<div className={css.imageWrapperMobile}>
<div className={css.aspectWrapper}>
<ResponsiveImage
rootClassName={css.rootForImage}
alt={listingTitle}
image={firstImage}
variants={['landscape-crop', 'landscape-crop2x']}
/>
</div>
</div>
<div className={classNames(css.avatarWrapperMobile, css.avatarMobile)}>
<AvatarMedium user={currentProvider} />
</div>
<DetailCardImage
rootClassName={css.imageWrapperMobile}
avatarWrapperClassName={css.avatarWrapperMobile}
listingTitle={listingTitle}
image={firstImage}
provider={currentProvider}
isCustomer={isCustomer}
/>
{isProvider ? (
<div className={css.avatarWrapperProviderDesktop}>
<AvatarLarge user={currentCustomer} className={css.avatarDesktop} />
</div>
) : null}
<TransactionPageTitle
transaction={currentTransaction}
customerDisplayName={customerDisplayName}
currentListing={currentListing}
listingTitle={listingTitle}
<PanelHeading
panelHeadingState={stateData.headingState}
transactionRole={transactionRole}
/>
<TransactionPageMessage
transaction={currentTransaction}
authorDisplayName={authorDisplayName}
customerDisplayName={customerDisplayName}
listingDeleted={listingDeleted}
providerName={authorDisplayName}
customerName={customerDisplayName}
isCustomerBanned={isCustomerBanned}
transactionRole={transactionRole}
listingId={currentListing.id && currentListing.id.uuid}
listingTitle={listingTitle}
listingDeleted={listingDeleted}
/>
<div className={css.bookingDetailsMobile}>
<div className={css.addressMobileWrapper}>
<AddressLinkMaybe
transaction={currentTransaction}
transactionRole={transactionRole}
currentListing={currentListing}
/>
</div>
<AddressLinkMaybe
rootClassName={css.addressMobile}
location={location}
geolocation={geolocation}
showAddress={stateData.showAddress}
/>
<BreakdownMaybe transaction={currentTransaction} transactionRole={transactionRole} />
</div>
<FeedSection
rootClassName={feedContainerClasses}
rootClassName={css.feedContainer}
currentTransaction={currentTransaction}
currentUser={currentUser}
fetchMessagesError={fetchMessagesError}
@ -276,13 +331,13 @@ export class TransactionPanelComponent extends Component {
messages={messages}
oldestMessagePageFetched={oldestMessagePageFetched}
onOpenReviewModal={this.onOpenReviewModal}
onShowMoreMessages={onShowMoreMessages}
onShowMoreMessages={() => onShowMoreMessages(currentTransaction.id)}
totalMessagePages={totalMessagePages}
/>
<SendMessageForm
form={this.sendMessageFormName}
rootClassName={sendMessageFormClasses}
rootClassName={css.sendMessageForm}
messagePlaceholder={sendMessagePlaceholder}
inProgress={sendMessageInProgress}
sendMessageError={sendMessageError}
@ -290,56 +345,51 @@ export class TransactionPanelComponent extends Component {
onBlur={this.onSendMessageFormBlur}
onSubmit={this.onMessageSubmit}
/>
{canShowSaleButtons ? (
{stateData.showSaleButtons ? (
<div className={css.mobileActionButtons}>{saleButtons}</div>
) : null}
</div>
<div className={css.asideDesktop}>
<div className={css.detailCard}>
<div className={css.detailCardImageWrapper}>
<div className={css.aspectWrapper}>
<ResponsiveImage
rootClassName={css.rootForImage}
alt={listingTitle}
image={firstImage}
variants={['landscape-crop', 'landscape-crop2x']}
/>
</div>
</div>
{isCustomer ? (
<div className={css.avatarWrapperCustomerDesktop}>
<AvatarMedium user={currentProvider} />
</div>
) : null}
<DetailCardImage
avatarWrapperClassName={css.avatarWrapperDesktop}
listingTitle={listingTitle}
image={firstImage}
provider={currentProvider}
isCustomer={isCustomer}
/>
<DetailCardHeadingsMaybe
transaction={currentTransaction}
transactionRole={transactionRole}
listing={currentListing}
showDetailCardHeadings={stateData.showDetailCardHeadings}
listingTitle={listingTitle}
subTitle={bookingSubTitle}
location={location}
geolocation={geolocation}
showAddress={stateData.showAddress}
/>
<BookingPanelMaybe
authorDisplayName={authorDisplayName}
transaction={currentTransaction}
transactionRole={transactionRole}
listing={currentListing}
listingTitle={listingTitle}
subTitle={bookingSubTitle}
provider={currentProvider}
onSubmit={onSubmitBookingRequest}
onManageDisableScrolling={onManageDisableScrolling}
timeSlots={timeSlots}
fetchTimeSlotsError={fetchTimeSlotsError}
/>
{stateData.showBookingPanel ? (
<BookingPanel
className={css.bookingPanel}
titleClassName={css.bookingTitle}
isOwnListing={false}
listing={currentListing}
title={listingTitle}
subTitle={bookingSubTitle}
authorDisplayName={authorDisplayName}
onSubmit={onSubmitBookingRequest}
onManageDisableScrolling={onManageDisableScrolling}
timeSlots={timeSlots}
fetchTimeSlotsError={fetchTimeSlotsError}
/>
) : null}
<BreakdownMaybe
className={css.breakdownContainer}
transaction={currentTransaction}
transactionRole={transactionRole}
/>
{canShowSaleButtons ? (
{stateData.showSaleButtons ? (
<div className={css.desktopActionButtons}>{saleButtons}</div>
) : null}
</div>