mirror of
https://github.com/kingomarnajjar/flex-template-web.git
synced 2026-07-30 18:16:48 +10:00
Split TransactionPanel to dummy subcomponents
and reduce direct state check agains current transaction.
This commit is contained in:
parent
538faf945d
commit
32fc6ba5b1
12 changed files with 1337 additions and 2438 deletions
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
29
src/components/TransactionPanel/AddressLinkMaybe.js
Normal file
29
src/components/TransactionPanel/AddressLinkMaybe.js
Normal 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;
|
||||
33
src/components/TransactionPanel/BreakdownMaybe.js
Normal file
33
src/components/TransactionPanel/BreakdownMaybe.js
Normal 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;
|
||||
26
src/components/TransactionPanel/DetailCardHeadingsMaybe.js
Normal file
26
src/components/TransactionPanel/DetailCardHeadingsMaybe.js
Normal 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;
|
||||
40
src/components/TransactionPanel/DetailCardImage.js
Normal file
40
src/components/TransactionPanel/DetailCardImage.js
Normal 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;
|
||||
64
src/components/TransactionPanel/FeedSection.js
Normal file
64
src/components/TransactionPanel/FeedSection.js
Normal 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;
|
||||
226
src/components/TransactionPanel/PanelHeading.js
Normal file
226
src/components/TransactionPanel/PanelHeading.js
Normal 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;
|
||||
64
src/components/TransactionPanel/SaleActionButtonsMaybe.js
Normal file
64
src/components/TransactionPanel/SaleActionButtonsMaybe.js
Normal 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;
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
Loading…
Add table
Reference in a new issue