mirror of
https://github.com/kingomarnajjar/flex-template-web.git
synced 2026-07-28 20:53:24 +10:00
394 lines
12 KiB
JavaScript
394 lines
12 KiB
JavaScript
import React from 'react';
|
|
import PropTypes from 'prop-types';
|
|
import { compose } from 'redux';
|
|
import { withRouter } from 'react-router-dom';
|
|
import { FormattedMessage, intlShape, injectIntl } from 'react-intl';
|
|
import classNames from 'classnames';
|
|
import routeConfiguration from '../../routeConfiguration';
|
|
import {
|
|
LINE_ITEM_NIGHT,
|
|
LINE_ITEM_DAY,
|
|
LISTING_STATE_PENDING_APPROVAL,
|
|
LISTING_STATE_CLOSED,
|
|
LISTING_STATE_DRAFT,
|
|
propTypes,
|
|
} from '../../util/types';
|
|
import { formatMoney } from '../../util/currency';
|
|
import { ensureOwnListing } from '../../util/data';
|
|
import {
|
|
LISTING_PAGE_PENDING_APPROVAL_VARIANT,
|
|
LISTING_PAGE_DRAFT_VARIANT,
|
|
LISTING_PAGE_PARAM_TYPE_DRAFT,
|
|
LISTING_PAGE_PARAM_TYPE_EDIT,
|
|
createSlug,
|
|
} from '../../util/urlHelpers';
|
|
import { createResourceLocatorString } from '../../util/routes';
|
|
import config from '../../config';
|
|
import {
|
|
InlineTextButton,
|
|
Menu,
|
|
MenuLabel,
|
|
MenuContent,
|
|
MenuItem,
|
|
NamedLink,
|
|
IconSpinner,
|
|
ResponsiveImage,
|
|
} from '../../components';
|
|
|
|
import MenuIcon from './MenuIcon';
|
|
import Overlay from './Overlay';
|
|
import css from './ManageListingCard.css';
|
|
|
|
// Menu content needs the same padding
|
|
const MENU_CONTENT_OFFSET = -12;
|
|
const MAX_LENGTH_FOR_WORDS_IN_TITLE = 7;
|
|
|
|
const priceData = (price, intl) => {
|
|
if (price && price.currency === config.currency) {
|
|
const formattedPrice = formatMoney(intl, price);
|
|
return { formattedPrice, priceTitle: formattedPrice };
|
|
} else if (price) {
|
|
return {
|
|
formattedPrice: intl.formatMessage(
|
|
{ id: 'ManageListingCard.unsupportedPrice' },
|
|
{ currency: price.currency }
|
|
),
|
|
priceTitle: intl.formatMessage(
|
|
{ id: 'ManageListingCard.unsupportedPriceTitle' },
|
|
{ currency: price.currency }
|
|
),
|
|
};
|
|
}
|
|
return {};
|
|
};
|
|
|
|
const createListingURL = (routes, listing) => {
|
|
const id = listing.id.uuid;
|
|
const slug = createSlug(listing.attributes.title);
|
|
const isPendingApproval = listing.attributes.state === LISTING_STATE_PENDING_APPROVAL;
|
|
const isDraft = listing.attributes.state === LISTING_STATE_DRAFT;
|
|
const variant = isDraft
|
|
? LISTING_PAGE_DRAFT_VARIANT
|
|
: isPendingApproval
|
|
? LISTING_PAGE_PENDING_APPROVAL_VARIANT
|
|
: null;
|
|
|
|
const linkProps =
|
|
isPendingApproval || isDraft
|
|
? {
|
|
name: 'ListingPageVariant',
|
|
params: {
|
|
id,
|
|
slug,
|
|
variant,
|
|
},
|
|
}
|
|
: {
|
|
name: 'ListingPage',
|
|
params: { id, slug },
|
|
};
|
|
|
|
return createResourceLocatorString(linkProps.name, routes, linkProps.params, {});
|
|
};
|
|
|
|
// Cards are not fixed sizes - So, long words in title make flexboxed items to grow too big.
|
|
// 1. We split title to an array of words and spaces.
|
|
// "foo bar".split(/([^\s]+)/gi) => ["", "foo", " ", "bar", ""]
|
|
// 2. Then we break long words by adding a '<span>' with word-break: 'break-all';
|
|
const formatTitle = (title, maxLength) => {
|
|
const nonWhiteSpaceSequence = /([^\s]+)/gi;
|
|
return title.split(nonWhiteSpaceSequence).map((word, index) => {
|
|
return word.length > maxLength ? (
|
|
<span key={index} style={{ wordBreak: 'break-all' }}>
|
|
{word}
|
|
</span>
|
|
) : (
|
|
word
|
|
);
|
|
});
|
|
};
|
|
|
|
export const ManageListingCardComponent = props => {
|
|
const {
|
|
className,
|
|
rootClassName,
|
|
hasClosingError,
|
|
hasOpeningError,
|
|
history,
|
|
intl,
|
|
isMenuOpen,
|
|
actionsInProgressListingId,
|
|
listing,
|
|
onCloseListing,
|
|
onOpenListing,
|
|
onToggleMenu,
|
|
renderSizes,
|
|
availabilityEnabled,
|
|
} = props;
|
|
const classes = classNames(rootClassName || css.root, className);
|
|
const currentListing = ensureOwnListing(listing);
|
|
const id = currentListing.id.uuid;
|
|
const { title = '', price, state } = currentListing.attributes;
|
|
const slug = createSlug(title);
|
|
const isPendingApproval = state === LISTING_STATE_PENDING_APPROVAL;
|
|
const isClosed = state === LISTING_STATE_CLOSED;
|
|
const isDraft = state === LISTING_STATE_DRAFT;
|
|
const firstImage =
|
|
currentListing.images && currentListing.images.length > 0 ? currentListing.images[0] : null;
|
|
|
|
const menuItemClasses = classNames(css.menuItem, {
|
|
[css.menuItemDisabled]: !!actionsInProgressListingId,
|
|
});
|
|
|
|
const { formattedPrice, priceTitle } = priceData(price, intl);
|
|
|
|
const hasError = hasOpeningError || hasClosingError;
|
|
const thisListingInProgress =
|
|
actionsInProgressListingId && actionsInProgressListingId.uuid === id;
|
|
|
|
const titleClasses = classNames(css.title, {
|
|
[css.titlePending]: isPendingApproval,
|
|
[css.titleDraft]: isDraft,
|
|
});
|
|
|
|
const editListingLinkType = isDraft
|
|
? LISTING_PAGE_PARAM_TYPE_DRAFT
|
|
: LISTING_PAGE_PARAM_TYPE_EDIT;
|
|
|
|
const unitType = config.bookingUnitType;
|
|
const isNightly = unitType === LINE_ITEM_NIGHT;
|
|
const isDaily = unitType === LINE_ITEM_DAY;
|
|
|
|
const unitTranslationKey = isNightly
|
|
? 'ManageListingCard.perNight'
|
|
: isDaily
|
|
? 'ManageListingCard.perDay'
|
|
: 'ManageListingCard.perUnit';
|
|
|
|
return (
|
|
<div className={classes}>
|
|
<div
|
|
className={css.threeToTwoWrapper}
|
|
tabIndex={0}
|
|
onClick={event => {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
|
|
// ManageListingCard contains links, buttons and elements that are working with routing.
|
|
// This card doesn't work if <a> or <button> is used to wrap events that are card 'clicks'.
|
|
//
|
|
// NOTE: It might be better to absolute-position those buttons over a card-links.
|
|
// (So, that they have no parent-child relationship - like '<a>bla<a>blaa</a></a>')
|
|
history.push(createListingURL(routeConfiguration(), listing));
|
|
}}
|
|
>
|
|
<div className={css.aspectWrapper}>
|
|
<ResponsiveImage
|
|
rootClassName={css.rootForImage}
|
|
alt={title}
|
|
image={firstImage}
|
|
variants={['landscape-crop', 'landscape-crop2x']}
|
|
sizes={renderSizes}
|
|
/>
|
|
</div>
|
|
<div className={classNames(css.menuOverlayWrapper, { [css.menuOverlayOpen]: isMenuOpen })}>
|
|
<div className={classNames(css.menuOverlay)} />
|
|
<div className={css.menuOverlayContent}>
|
|
<FormattedMessage id="ManageListingCard.viewListing" />
|
|
</div>
|
|
</div>
|
|
<div className={css.menubarWrapper}>
|
|
<div className={css.menubarGradient} />
|
|
<div className={css.menubar}>
|
|
<Menu
|
|
className={classNames(css.menu, { [css.cardIsOpen]: !isClosed })}
|
|
contentPlacementOffset={MENU_CONTENT_OFFSET}
|
|
contentPosition="left"
|
|
useArrow={false}
|
|
onToggleActive={isOpen => {
|
|
const listingOpen = isOpen ? currentListing : null;
|
|
onToggleMenu(listingOpen);
|
|
}}
|
|
isOpen={isMenuOpen}
|
|
>
|
|
<MenuLabel className={css.menuLabel} isOpenClassName={css.listingMenuIsOpen}>
|
|
<div className={css.iconWrapper}>
|
|
<MenuIcon className={css.menuIcon} isActive={isMenuOpen} />
|
|
</div>
|
|
</MenuLabel>
|
|
<MenuContent rootClassName={css.menuContent}>
|
|
<MenuItem key="close-listing">
|
|
<InlineTextButton
|
|
className={menuItemClasses}
|
|
onClick={event => {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
if (!actionsInProgressListingId) {
|
|
onToggleMenu(null);
|
|
onCloseListing(currentListing.id);
|
|
}
|
|
}}
|
|
>
|
|
<FormattedMessage id="ManageListingCard.closeListing" />
|
|
</InlineTextButton>
|
|
</MenuItem>
|
|
</MenuContent>
|
|
</Menu>
|
|
</div>
|
|
</div>
|
|
{isDraft ? (
|
|
<React.Fragment>
|
|
<div className={classNames({ [css.draftNoImage]: !firstImage })} />
|
|
<Overlay
|
|
message={intl.formatMessage(
|
|
{ id: 'ManageListingCard.draftOverlayText' },
|
|
{ listingTitle: title }
|
|
)}
|
|
>
|
|
<NamedLink
|
|
className={css.finishListingDraftLink}
|
|
name="EditListingPage"
|
|
params={{ id, slug, type: LISTING_PAGE_PARAM_TYPE_DRAFT, tab: 'photos' }}
|
|
>
|
|
<FormattedMessage id="ManageListingCard.finishListingDraft" />
|
|
</NamedLink>
|
|
</Overlay>
|
|
</React.Fragment>
|
|
) : null}
|
|
{isClosed ? (
|
|
<Overlay
|
|
message={intl.formatMessage(
|
|
{ id: 'ManageListingCard.closedListing' },
|
|
{ listingTitle: title }
|
|
)}
|
|
>
|
|
<button
|
|
className={css.openListingButton}
|
|
disabled={!!actionsInProgressListingId}
|
|
onClick={event => {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
if (!actionsInProgressListingId) {
|
|
onOpenListing(currentListing.id);
|
|
}
|
|
}}
|
|
>
|
|
<FormattedMessage id="ManageListingCard.openListing" />
|
|
</button>
|
|
</Overlay>
|
|
) : null}
|
|
{isPendingApproval ? (
|
|
<Overlay
|
|
message={intl.formatMessage(
|
|
{ id: 'ManageListingCard.pendingApproval' },
|
|
{ listingTitle: title }
|
|
)}
|
|
/>
|
|
) : null}
|
|
{thisListingInProgress ? (
|
|
<Overlay>
|
|
<IconSpinner />
|
|
</Overlay>
|
|
) : hasError ? (
|
|
<Overlay errorMessage={intl.formatMessage({ id: 'ManageListingCard.actionFailed' })} />
|
|
) : null}
|
|
</div>
|
|
|
|
<div className={css.info}>
|
|
<div className={css.price}>
|
|
{formattedPrice ? (
|
|
<React.Fragment>
|
|
<div className={css.priceValue} title={priceTitle}>
|
|
{formattedPrice}
|
|
</div>
|
|
<div className={css.perUnit}>
|
|
<FormattedMessage id={unitTranslationKey} />
|
|
</div>
|
|
</React.Fragment>
|
|
) : (
|
|
<div className={css.noPrice}>
|
|
<FormattedMessage id="ManageListingCard.priceNotSet" />
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className={css.mainInfo}>
|
|
<div className={css.titleWrapper}>
|
|
<InlineTextButton
|
|
className={titleClasses}
|
|
onClick={event => {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
history.push(createListingURL(routeConfiguration(), listing));
|
|
}}
|
|
>
|
|
{formatTitle(title, MAX_LENGTH_FOR_WORDS_IN_TITLE)}
|
|
</InlineTextButton>
|
|
</div>
|
|
</div>
|
|
|
|
<div className={css.manageLinks}>
|
|
<NamedLink
|
|
className={css.manageLink}
|
|
name="EditListingPage"
|
|
params={{ id, slug, type: editListingLinkType, tab: 'description' }}
|
|
>
|
|
<FormattedMessage id="ManageListingCard.editListing" />
|
|
</NamedLink>
|
|
|
|
{availabilityEnabled ? (
|
|
<React.Fragment>
|
|
<span className={css.manageLinksSeparator}>{' • '}</span>
|
|
|
|
<NamedLink
|
|
className={css.manageLink}
|
|
name="EditListingPage"
|
|
params={{ id, slug, type: editListingLinkType, tab: 'availability' }}
|
|
>
|
|
<FormattedMessage id="ManageListingCard.manageAvailability" />
|
|
</NamedLink>
|
|
</React.Fragment>
|
|
) : null}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
ManageListingCardComponent.defaultProps = {
|
|
className: null,
|
|
rootClassName: null,
|
|
actionsInProgressListingId: null,
|
|
renderSizes: null,
|
|
availabilityEnabled: config.enableAvailability,
|
|
};
|
|
|
|
const { bool, func, shape, string } = PropTypes;
|
|
|
|
ManageListingCardComponent.propTypes = {
|
|
className: string,
|
|
rootClassName: string,
|
|
hasClosingError: bool.isRequired,
|
|
hasOpeningError: bool.isRequired,
|
|
intl: intlShape.isRequired,
|
|
listing: propTypes.ownListing.isRequired,
|
|
isMenuOpen: bool.isRequired,
|
|
actionsInProgressListingId: shape({ uuid: string.isRequired }),
|
|
onCloseListing: func.isRequired,
|
|
onOpenListing: func.isRequired,
|
|
onToggleMenu: func.isRequired,
|
|
availabilityEnabled: bool,
|
|
|
|
// Responsive image sizes hint
|
|
renderSizes: string,
|
|
|
|
// from withRouter
|
|
history: shape({
|
|
push: func.isRequired,
|
|
}).isRequired,
|
|
};
|
|
|
|
export default compose(
|
|
withRouter,
|
|
injectIntl
|
|
)(ManageListingCardComponent);
|