mirror of
https://github.com/kingomarnajjar/flex-template-web.git
synced 2026-07-31 02:26:50 +10:00
Merge pull request #370 from sharetribe/managelistingspage-open-close-listing
Managelistingspage open close listing
This commit is contained in:
commit
220cb38afa
17 changed files with 872 additions and 118 deletions
|
|
@ -34,7 +34,7 @@
|
|||
"redux-thunk": "^2.2.0",
|
||||
"sanitize.css": "^5.0.0",
|
||||
"sharetribe-scripts": "0.9.2",
|
||||
"sharetribe-sdk": "git+ssh://git@github.com/sharetribe/sharetribe-sdk-js#ec9fd243307ce21b95052535d2e358f681ed82b7",
|
||||
"sharetribe-sdk": "git+ssh://git@github.com/sharetribe/sharetribe-sdk-js#75c5b1a9df5142a1066cd055c2157b410f4c4aee",
|
||||
"source-map-support": "^0.4.15",
|
||||
"url": "^0.11.0"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,9 +1,20 @@
|
|||
@import '../../marketplace.css';
|
||||
|
||||
:root {
|
||||
--coverEverything: {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.root {
|
||||
/* Layout */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
|
||||
/* Remove link's hover effect */
|
||||
&:hover {
|
||||
|
|
@ -11,37 +22,176 @@
|
|||
}
|
||||
}
|
||||
|
||||
.menuOverlayWrapper {
|
||||
/* Positioning */
|
||||
@apply --coverEverything;
|
||||
}
|
||||
|
||||
.menuOverlay {
|
||||
/* Positioning */
|
||||
@apply --coverEverything;
|
||||
|
||||
/* Overlay */
|
||||
background-color: var(--matterColorAnti);
|
||||
mix-blend-mode: multiply;
|
||||
opacity: 0;
|
||||
transition: var(--transitionStyleButton);
|
||||
}
|
||||
|
||||
.menuOverlayContent {
|
||||
/* Positioning */
|
||||
@apply --coverEverything;
|
||||
/* Fonts */
|
||||
@apply --marketplaceH4FontStyles;
|
||||
font-weight: var(--fontWeightSemiBold);
|
||||
color: var(--matterColorLight);
|
||||
|
||||
/* Layout */
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
opacity: 0;
|
||||
|
||||
@media (--viewportMedium) {
|
||||
font-weight: var(--fontWeightSemiBold);
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.threeToTwoWrapper {
|
||||
/* Layout */
|
||||
display: block;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
|
||||
&:hover .menuOverlay {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
&:hover .menuOverlayContent {
|
||||
opacity: 1.0;
|
||||
}
|
||||
}
|
||||
|
||||
.menuOverlayOpen {
|
||||
& .menuOverlay {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
& .menuOverlayContent {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
/* Firefox doesn't support image aspect ratio inside flexbox */
|
||||
.aspectWrapper {
|
||||
padding-bottom: 66.6667%; /* 3:2 Aspect Ratio */
|
||||
background: var(--matterColorNegative); /* Loading BG color */
|
||||
transition: var(--transitionStyleButton);
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.02);
|
||||
box-shadow: var(--boxShadowListingCard);
|
||||
transition: var(--transitionStyleButton);
|
||||
}
|
||||
}
|
||||
|
||||
.rootForImage {
|
||||
/* Layout - image will take space defined by aspect ratio wrapper */
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
@apply --coverEverything;
|
||||
width: 100%;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.menubarWrapper {
|
||||
/* Position */
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 42px;
|
||||
}
|
||||
|
||||
.menubarGradient {
|
||||
/* Position */
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
/* Gradient */
|
||||
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAAqCAYAAACUV/o4AAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAALEwAACxMBAJqcGAAAAVlpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IlhNUCBDb3JlIDUuNC4wIj4KICAgPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICAgICAgPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIKICAgICAgICAgICAgeG1sbnM6dGlmZj0iaHR0cDovL25zLmFkb2JlLmNvbS90aWZmLzEuMC8iPgogICAgICAgICA8dGlmZjpPcmllbnRhdGlvbj4xPC90aWZmOk9yaWVudGF0aW9uPgogICAgICA8L3JkZjpEZXNjcmlwdGlvbj4KICAgPC9yZGY6UkRGPgo8L3g6eG1wbWV0YT4KTMInWQAAAGhJREFUKBVjZGBgKANibiAGga8sQAKEf4N4QMAC4vwH4r8gHgiABOAcmMAfEAMGsKrA0DIqAAsvHGFKkwACRRQjzGJQRP2DcUA0LOZAEQ5S9R8mAJIEA6wCyIYSqYVSz4FcxwR1E5gCAD7HLabS/HFUAAAAAElFTkSuQmCC');
|
||||
background-repeat: repeat;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.menubar {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.menuLabel {
|
||||
padding: 8px 8px 0 8px;
|
||||
}
|
||||
|
||||
.iconWrapper {
|
||||
padding: 2px 10px 8px 10px;
|
||||
color: var(--matterColorLight);
|
||||
border-radius: 2px 2px 0 0;
|
||||
}
|
||||
|
||||
.listingMenuIsOpen {
|
||||
& .iconWrapper {
|
||||
background-color: var(--matterColorLight);
|
||||
color: var(--matterColor);
|
||||
}
|
||||
}
|
||||
|
||||
.menuContent {
|
||||
visibility:hidden;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
|
||||
position: absolute;
|
||||
z-index: var(--zIndexPopup);
|
||||
|
||||
background-color: var(--matterColorLight);
|
||||
border-radius: 2px 0 2px 2px;
|
||||
}
|
||||
|
||||
.menuItem {
|
||||
@apply --marketplaceH4FontStyles;
|
||||
color: var(--matterColor);
|
||||
white-space: nowrap;
|
||||
text-align: left;
|
||||
min-width: 175px;
|
||||
padding: 8px 14px;
|
||||
|
||||
/* Remove default margins from font */
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
|
||||
&:hover {
|
||||
color: var(--failColor);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
@media (--viewportMedium) {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.menuItemDisabled {
|
||||
color: var(--matterColorNegative);
|
||||
|
||||
&:hover {
|
||||
color: var(--matterColorNegative);
|
||||
}
|
||||
}
|
||||
|
||||
.info {
|
||||
/* Layout */
|
||||
display: flex;
|
||||
|
|
@ -125,3 +275,57 @@
|
|||
background-repeat: no-repeat;
|
||||
background-position: 15px center;
|
||||
}
|
||||
|
||||
.loadingOverlayWrapper,
|
||||
.closedOverlayWrapper {
|
||||
/* Positioning */
|
||||
@apply --coverEverything;
|
||||
}
|
||||
|
||||
.loadingOverlay,
|
||||
.closedOverlay {
|
||||
/* Positioning */
|
||||
@apply --coverEverything;
|
||||
|
||||
/* Overlay background style */
|
||||
background-color: var(--matterColorLight);
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.loadingOverlayContent,
|
||||
.closedOverlayContent {
|
||||
@apply --marketplaceTinyFontStyles;
|
||||
color: var(--matterColor);
|
||||
|
||||
/* Positioning */
|
||||
@apply --coverEverything;
|
||||
|
||||
/* Layout */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: default;
|
||||
|
||||
/* Remove default margins from font */
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
|
||||
@media (--viewportMedium) {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.closedMessage {
|
||||
max-width: 180px;
|
||||
text-align: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.openListingButton {
|
||||
@apply --marketplaceH5FontStyles;
|
||||
width: 114px;
|
||||
height: 41px;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import React from 'react';
|
|||
import ManageListingCard from './ManageListingCard';
|
||||
import { createOwnListing, fakeIntl } from '../../util/test-data';
|
||||
|
||||
const noop = () => null;
|
||||
const listing = { ...createOwnListing('listing1') };
|
||||
|
||||
const ManageListingCardWrapper = props => (
|
||||
|
|
@ -16,5 +17,11 @@ export const ManageListingCardWrapped = {
|
|||
props: {
|
||||
intl: fakeIntl,
|
||||
listing,
|
||||
isMenuOpen: false,
|
||||
onCloseListing: noop,
|
||||
onOpenListing: noop,
|
||||
onToggleMenu: noop,
|
||||
flattenedRoutes: [],
|
||||
history: { push: noop },
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -9,11 +9,26 @@ import { ensureListing } from '../../util/data';
|
|||
import { createSlug } from '../../util/urlHelpers';
|
||||
import { withFlattenedRoutes } from '../../util/contextHelpers';
|
||||
import { createResourceLocatorString } from '../../util/routes';
|
||||
import { NamedLink, SecondaryButton, ResponsiveImage } from '../../components';
|
||||
import {
|
||||
InlineTextButton,
|
||||
Menu,
|
||||
MenuLabel,
|
||||
MenuContent,
|
||||
MenuItem,
|
||||
NamedLink,
|
||||
PrimaryButton,
|
||||
SecondaryButton,
|
||||
SpinnerIcon,
|
||||
ResponsiveImage,
|
||||
} from '../../components';
|
||||
import config from '../../config';
|
||||
|
||||
import MenuIcon from './MenuIcon';
|
||||
import css from './ManageListingCard.css';
|
||||
|
||||
// Menu content needs the same padding
|
||||
const MENU_CONTENT_OFFSET = 8;
|
||||
|
||||
const priceData = (price, intl) => {
|
||||
if (price && price.currency === config.currency) {
|
||||
const formattedPrice = formatMoney(intl, price);
|
||||
|
|
@ -48,20 +63,77 @@ export const ManageListingCardComponent = props => {
|
|||
flattenedRoutes,
|
||||
history,
|
||||
intl,
|
||||
isMenuOpen,
|
||||
actionsInProgressListingId,
|
||||
listing,
|
||||
onCloseListing,
|
||||
onOpenListing,
|
||||
onToggleMenu,
|
||||
} = props;
|
||||
const classes = classNames(rootClassName || css.root, className);
|
||||
const currentListing = ensureListing(listing);
|
||||
const id = currentListing.id.uuid;
|
||||
const { title = '', price } = currentListing.attributes;
|
||||
const { title = '', price, open } = currentListing.attributes;
|
||||
const slug = createSlug(title);
|
||||
const firstImage = currentListing.images && currentListing.images.length > 0
|
||||
? currentListing.images[0]
|
||||
: null;
|
||||
|
||||
const menuItemClasses = classNames(css.menuItem, {
|
||||
[css.menuItemDisabled]: !!actionsInProgressListingId,
|
||||
});
|
||||
|
||||
// TODO: Currently, API can return currencies that are not supported by starter app.
|
||||
const { formattedPrice, priceTitle } = priceData(price, intl);
|
||||
|
||||
/* eslint-disable jsx-a11y/no-static-element-interactions */
|
||||
const closedOverlay = open
|
||||
? null
|
||||
: <div
|
||||
className={css.closedOverlayWrapper}
|
||||
onClick={event => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<div className={css.closedOverlay} />
|
||||
<div className={css.closedOverlayContent}>
|
||||
<div className={css.closedMessage}>
|
||||
<FormattedMessage id="ManageListingCard.closedListing" />
|
||||
</div>
|
||||
<PrimaryButton
|
||||
className={css.openListingButton}
|
||||
disabled={!!actionsInProgressListingId}
|
||||
onClick={event => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if (!actionsInProgressListingId) {
|
||||
onOpenListing(currentListing.id);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<FormattedMessage id="ManageListingCard.openListing" />
|
||||
</PrimaryButton>
|
||||
</div>
|
||||
</div>;
|
||||
|
||||
const thisInProgress = actionsInProgressListingId && actionsInProgressListingId.uuid === id;
|
||||
const loadingOrClosedOverlay = thisInProgress
|
||||
? <div
|
||||
className={css.loadingOverlayWrapper}
|
||||
onClick={event => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<div className={css.loadingOverlay} />
|
||||
<div className={css.loadingOverlayContent}>
|
||||
<SpinnerIcon />
|
||||
</div>
|
||||
</div>
|
||||
: closedOverlay;
|
||||
/* eslint-enable jsx-a11y/no-static-element-interactions */
|
||||
|
||||
return (
|
||||
<NamedLink className={classes} name="ListingPage" params={{ id, slug }}>
|
||||
<div className={css.threeToTwoWrapper}>
|
||||
|
|
@ -76,6 +148,50 @@ export const ManageListingCardComponent = props => {
|
|||
]}
|
||||
/>
|
||||
</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
|
||||
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>
|
||||
</div>
|
||||
<div className={css.info}>
|
||||
<div className={css.price}>
|
||||
|
|
@ -102,6 +218,7 @@ export const ManageListingCardComponent = props => {
|
|||
<FormattedMessage id="ManageListingCard.edit" />
|
||||
</SecondaryButton>
|
||||
</div>
|
||||
{loadingOrClosedOverlay}
|
||||
</NamedLink>
|
||||
);
|
||||
};
|
||||
|
|
@ -109,15 +226,21 @@ export const ManageListingCardComponent = props => {
|
|||
ManageListingCardComponent.defaultProps = {
|
||||
className: null,
|
||||
rootClassName: null,
|
||||
actionsInProgressListingId: null,
|
||||
};
|
||||
|
||||
const { arrayOf, func, shape, string } = PropTypes;
|
||||
const { arrayOf, bool, func, shape, string } = PropTypes;
|
||||
|
||||
ManageListingCardComponent.propTypes = {
|
||||
className: string,
|
||||
rootClassName: string,
|
||||
intl: intlShape.isRequired,
|
||||
listing: propTypes.listing.isRequired,
|
||||
isMenuOpen: bool.isRequired,
|
||||
actionsInProgressListingId: shape({ uuid: string.isRequired }),
|
||||
onCloseListing: func.isRequired,
|
||||
onOpenListing: func.isRequired,
|
||||
onToggleMenu: func.isRequired,
|
||||
|
||||
// from withFlattenedRoutes
|
||||
flattenedRoutes: arrayOf(propTypes.route).isRequired,
|
||||
|
|
|
|||
|
|
@ -14,6 +14,10 @@ describe('ManageListingCard', () => {
|
|||
history={{ push: noop }}
|
||||
listing={listing}
|
||||
intl={fakeIntl}
|
||||
isMenuOpen={false}
|
||||
onCloseListing={noop}
|
||||
onOpenListing={noop}
|
||||
onToggleMenu={noop}
|
||||
/>
|
||||
);
|
||||
expect(tree).toMatchSnapshot();
|
||||
|
|
|
|||
54
src/components/ManageListingCard/MenuIcon.js
Normal file
54
src/components/ManageListingCard/MenuIcon.js
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
import React, { PropTypes } from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import css from './ManageListingCard.css';
|
||||
|
||||
const MenuIcon = props => {
|
||||
const { className, isActive } = props;
|
||||
const classes = classNames(css.menuIcon, className);
|
||||
const filter = isActive ? '' : 'url(#a)';
|
||||
return (
|
||||
<svg
|
||||
className={classes}
|
||||
width="26"
|
||||
height="12"
|
||||
viewBox="0 0 26 12"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<defs>
|
||||
<filter
|
||||
x="-38.9%"
|
||||
y="-125%"
|
||||
width="177.8%"
|
||||
height="450%"
|
||||
filterUnits="objectBoundingBox"
|
||||
id="a"
|
||||
>
|
||||
<feOffset dy="2" in="SourceAlpha" result="shadowOffsetOuter1" />
|
||||
<feGaussianBlur stdDeviation="2" in="shadowOffsetOuter1" result="shadowBlurOuter1" />
|
||||
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.5 0" in="shadowBlurOuter1" />
|
||||
<feBlend in="SourceGraphic" in2="blurOut" mode="normal" />
|
||||
</filter>
|
||||
</defs>
|
||||
<g transform="translate(-342 -18)" filter={filter}>
|
||||
<path
|
||||
d="M348 24c-1.105 0-2-.895-2-2s.895-2 2-2 2 .895 2 2-.895 2-2 2zm7 0c-1.105 0-2-.895-2-2s.895-2 2-2 2 .895 2 2-.895 2-2 2zm7 0c-1.105 0-2-.895-2-2s.895-2 2-2 2 .895 2 2-.895 2-2 2z"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
MenuIcon.defaultProps = {
|
||||
className: null,
|
||||
isActive: false,
|
||||
};
|
||||
|
||||
const { bool, string } = PropTypes;
|
||||
|
||||
MenuIcon.propTypes = {
|
||||
className: string,
|
||||
isActive: bool,
|
||||
};
|
||||
|
||||
export default MenuIcon;
|
||||
|
|
@ -30,6 +30,62 @@ exports[`ManageListingCard matches snapshot 1`] = `
|
|||
rootClassName={null}
|
||||
sizes={null} />
|
||||
</div>
|
||||
<div
|
||||
className="">
|
||||
<div
|
||||
className="" />
|
||||
<div>
|
||||
<FormattedMessage
|
||||
id="ManageListingCard.viewListing"
|
||||
values={Object {}} />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div />
|
||||
<div>
|
||||
<Menu
|
||||
className={null}
|
||||
contentPlacementOffset={8}
|
||||
contentPosition="left"
|
||||
isOpen={false}
|
||||
onToggleActive={[Function]}
|
||||
rootClassName=""
|
||||
useArrow={false}>
|
||||
<MenuLabel
|
||||
className={null}
|
||||
isOpen={false}
|
||||
isOpenClassName={null}
|
||||
onToggleActive={null}
|
||||
rootClassName="">
|
||||
<div>
|
||||
<MenuIcon
|
||||
className={null}
|
||||
isActive={false} />
|
||||
</div>
|
||||
</MenuLabel>
|
||||
<MenuContent
|
||||
arrowPosition={null}
|
||||
className={null}
|
||||
contentClassName={null}
|
||||
contentRef={null}
|
||||
isOpen={false}
|
||||
rootClassName=""
|
||||
style={null}>
|
||||
<MenuItem
|
||||
className={null}
|
||||
rootClassName="">
|
||||
<InlineTextButton
|
||||
className=""
|
||||
onClick={[Function]}>
|
||||
<FormattedMessage
|
||||
id="ManageListingCard.closeListing"
|
||||
values={Object {}} />
|
||||
</InlineTextButton>
|
||||
</MenuItem>
|
||||
</MenuContent>
|
||||
</Menu>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div>
|
||||
|
|
|
|||
|
|
@ -24,6 +24,12 @@ import css from './Menu.css';
|
|||
|
||||
const KEY_CODE_ESCAPE = 27;
|
||||
const CONTENT_PLACEMENT_OFFSET = 0;
|
||||
const CONTENT_TO_LEFT = 'left';
|
||||
const CONTENT_TO_RIGHT = 'right';
|
||||
|
||||
const isControlledMenu = (isOpenProp, onToggleActiveProp) => {
|
||||
return isOpenProp !== null && onToggleActiveProp !== null;
|
||||
};
|
||||
|
||||
// This should work, but it doesn't <div className="foo" onClick={() => {}} role="button" />
|
||||
/* eslint-disable jsx-a11y/no-static-element-interactions */
|
||||
|
|
@ -33,6 +39,16 @@ class Menu extends Component {
|
|||
|
||||
this.state = { isOpen: false };
|
||||
|
||||
const { isOpen, onToggleActive } = props;
|
||||
const isIndependentMenu = isOpen === null && onToggleActive === null;
|
||||
if (!(isIndependentMenu || isControlledMenu(isOpen, onToggleActive))) {
|
||||
throw new Error(
|
||||
`Menu has invalid props:
|
||||
Both isOpen and onToggleActive need to be defined (controlled menu),
|
||||
or neither of them (menu uses its own state management).`
|
||||
);
|
||||
}
|
||||
|
||||
this.onBlur = this.onBlur.bind(this);
|
||||
this.onKeyDown = this.onKeyDown.bind(this);
|
||||
this.toggleOpen = this.toggleOpen.bind(this);
|
||||
|
|
@ -60,20 +76,30 @@ class Menu extends Component {
|
|||
}
|
||||
|
||||
toggleOpen(enforcedState) {
|
||||
this.setState(prevState => {
|
||||
const isOpen = enforcedState != null ? enforcedState : !prevState.isOpen;
|
||||
return { isOpen };
|
||||
});
|
||||
// If state is handled outside of Menu component, we call a passed in onToggleActive func
|
||||
const { isOpen, onToggleActive } = this.props;
|
||||
if (isControlledMenu(isOpen, onToggleActive)) {
|
||||
const isMenuOpen = enforcedState != null ? enforcedState : !isOpen;
|
||||
onToggleActive(isMenuOpen);
|
||||
} else {
|
||||
// If state is handled inside of Menu component, set state
|
||||
this.setState(prevState => {
|
||||
const isMenuOpen = enforcedState != null ? enforcedState : !prevState.isOpen;
|
||||
return { isOpen: isMenuOpen };
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
positionStyleForMenuContent() {
|
||||
positionStyleForMenuContent(contentPosition) {
|
||||
if (this.menu && this.menuContent) {
|
||||
// Calculate wether we should show the menu to the left of the component or right
|
||||
const distanceToRight = window.innerWidth - this.menu.getBoundingClientRect().right;
|
||||
const menuWidth = this.menu.offsetWidth;
|
||||
const contentWidthBiggerThanLabel = this.menuContent.offsetWidth - menuWidth;
|
||||
return distanceToRight < contentWidthBiggerThanLabel
|
||||
? { right: -1 * CONTENT_PLACEMENT_OFFSET, minWidth: menuWidth }
|
||||
const usePositionRightFromLabel = contentPosition === CONTENT_TO_LEFT;
|
||||
const contentPlacementOffset = this.props.contentPlacementOffset;
|
||||
return usePositionRightFromLabel || distanceToRight < contentWidthBiggerThanLabel
|
||||
? { right: contentPlacementOffset, minWidth: menuWidth }
|
||||
: { left: 0, minWidth: menuWidth };
|
||||
}
|
||||
return {};
|
||||
|
|
@ -82,8 +108,9 @@ class Menu extends Component {
|
|||
positionStyleForArrow(isPositionedRight) {
|
||||
if (this.menu) {
|
||||
const menuWidth = this.menu.offsetWidth;
|
||||
const contentPlacementOffset = this.props.contentPlacementOffset;
|
||||
return isPositionedRight
|
||||
? Math.floor(menuWidth / 2) + CONTENT_PLACEMENT_OFFSET
|
||||
? Math.floor(menuWidth / 2) - contentPlacementOffset
|
||||
: Math.floor(menuWidth / 2);
|
||||
}
|
||||
return 0;
|
||||
|
|
@ -95,23 +122,30 @@ class Menu extends Component {
|
|||
}
|
||||
|
||||
return React.Children.map(this.props.children, child => {
|
||||
const { isOpen: isOpenProp, onToggleActive } = this.props;
|
||||
const isOpen = isControlledMenu(isOpenProp, onToggleActive) ? isOpenProp : this.state.isOpen;
|
||||
|
||||
if (child.type === MenuLabel) {
|
||||
// MenuLabel needs toggleOpen function
|
||||
// We pass that directly so that component user doesn't need to worry about that
|
||||
return React.cloneElement(child, {
|
||||
isOpen: this.state.isOpen,
|
||||
isOpen,
|
||||
onToggleActive: this.toggleOpen,
|
||||
});
|
||||
} else if (child.type === MenuContent) {
|
||||
// MenuContent needs some styling data (width, arrowPosition, and isOpen info)
|
||||
// We pass those directly so that component user doesn't need to worry about those.
|
||||
const positionStyles = this.positionStyleForMenuContent();
|
||||
const { contentPosition, useArrow } = this.props;
|
||||
const positionStyles = this.positionStyleForMenuContent(contentPosition);
|
||||
const arrowPosition = useArrow
|
||||
? this.positionStyleForArrow(positionStyles.right != null)
|
||||
: null;
|
||||
return React.cloneElement(child, {
|
||||
arrowPosition: this.positionStyleForArrow(positionStyles.right != null),
|
||||
arrowPosition,
|
||||
contentRef: node => {
|
||||
this.menuContent = node;
|
||||
},
|
||||
isOpen: this.state.isOpen,
|
||||
isOpen,
|
||||
style: { ...child.props.style, ...positionStyles },
|
||||
});
|
||||
} else {
|
||||
|
|
@ -142,14 +176,27 @@ class Menu extends Component {
|
|||
}
|
||||
/* eslint-enable jsx-a11y/no-static-element-interactions */
|
||||
|
||||
Menu.defaultProps = { className: null, rootClassName: '' };
|
||||
Menu.defaultProps = {
|
||||
className: null,
|
||||
rootClassName: '',
|
||||
contentPlacementOffset: CONTENT_PLACEMENT_OFFSET,
|
||||
contentPosition: CONTENT_TO_RIGHT,
|
||||
isOpen: null,
|
||||
onToggleActive: null,
|
||||
useArrow: true,
|
||||
};
|
||||
|
||||
const { node, string } = PropTypes;
|
||||
const { bool, func, node, number, string } = PropTypes;
|
||||
|
||||
Menu.propTypes = {
|
||||
children: node.isRequired,
|
||||
className: string,
|
||||
rootClassName: string,
|
||||
contentPosition: string,
|
||||
contentPlacementOffset: number,
|
||||
useArrow: bool,
|
||||
isOpen: bool,
|
||||
onToggleActive: func,
|
||||
};
|
||||
|
||||
export default Menu;
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@ class MenuLabel extends Component {
|
|||
}
|
||||
|
||||
onClick(e) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.props.onToggleActive();
|
||||
|
||||
// Don't show focus outline if user just clicked the element with mouse
|
||||
|
|
|
|||
7
src/components/SpinnerIcon/SpinnerIcon.css
Normal file
7
src/components/SpinnerIcon/SpinnerIcon.css
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
@import '../../marketplace.css';
|
||||
|
||||
.root {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
stroke: var(--marketplaceColor);
|
||||
}
|
||||
55
src/components/SpinnerIcon/SpinnerIcon.js
Normal file
55
src/components/SpinnerIcon/SpinnerIcon.js
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
import React, { PropTypes } from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import css from './SpinnerIcon.css';
|
||||
|
||||
// TODO: SVG needs to be changed so that it doesn't take 100x100 area,
|
||||
// but just a little bit more than what actual spinner takes (~26px)
|
||||
const SpinnerIcon = props => {
|
||||
const { rootClassName, className } = props;
|
||||
const classes = classNames(rootClassName || css.root, className);
|
||||
return (
|
||||
<svg
|
||||
className={classes}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 100 100"
|
||||
preserveAspectRatio="xMidYMid"
|
||||
>
|
||||
<circle cx="50" cy="50" fill="none" r="10" strokeWidth="3" strokeLinecap="round">
|
||||
<animateTransform
|
||||
attributeName="transform"
|
||||
type="rotate"
|
||||
calcMode="linear"
|
||||
values="0 50 50;180 50 50;720 50 50"
|
||||
keyTimes="0;0.5;1"
|
||||
dur="1s"
|
||||
begin="0s"
|
||||
repeatCount="indefinite"
|
||||
/>
|
||||
<animate
|
||||
attributeName="stroke-dasharray"
|
||||
calcMode="linear"
|
||||
values="6 56;46 14;6 56"
|
||||
keyTimes="0;0.5;1"
|
||||
dur="1"
|
||||
begin="0s"
|
||||
repeatCount="indefinite"
|
||||
/>
|
||||
</circle>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
SpinnerIcon.defaultProps = {
|
||||
rootClassName: null,
|
||||
className: null,
|
||||
};
|
||||
|
||||
const { string } = PropTypes;
|
||||
|
||||
SpinnerIcon.propTypes = {
|
||||
rootClassName: string,
|
||||
className: string,
|
||||
};
|
||||
|
||||
export default SpinnerIcon;
|
||||
|
|
@ -49,6 +49,7 @@ import SearchMapInfoCard from './SearchMapInfoCard/SearchMapInfoCard';
|
|||
import SearchMapPriceLabel from './SearchMapPriceLabel/SearchMapPriceLabel';
|
||||
import SearchResultsPanel from './SearchResultsPanel/SearchResultsPanel';
|
||||
import SelectField from './SelectField/SelectField';
|
||||
import SpinnerIcon from './SpinnerIcon/SpinnerIcon';
|
||||
import StripeBankAccountTokenInputField
|
||||
from './StripeBankAccountTokenInputField/StripeBankAccountTokenInputField';
|
||||
import TabNav from './TabNav/TabNav';
|
||||
|
|
@ -117,6 +118,7 @@ export {
|
|||
SearchResultsPanel,
|
||||
SecondaryButton,
|
||||
SelectField,
|
||||
SpinnerIcon,
|
||||
StripeBankAccountTokenInputField,
|
||||
TabNav,
|
||||
TabNavHorizontal,
|
||||
|
|
|
|||
|
|
@ -6,6 +6,14 @@ export const FETCH_LISTINGS_REQUEST = 'app/ManageListingsPage/FETCH_LISTINGS_REQ
|
|||
export const FETCH_LISTINGS_SUCCESS = 'app/ManageListingsPage/FETCH_LISTINGS_SUCCESS';
|
||||
export const FETCH_LISTINGS_ERROR = 'app/ManageListingsPage/FETCH_LISTINGS_ERROR';
|
||||
|
||||
export const OPEN_LISTING_REQUEST = 'app/ManageListingsPage/OPEN_LISTING_REQUEST';
|
||||
export const OPEN_LISTING_SUCCESS = 'app/ManageListingsPage/OPEN_LISTING_SUCCESS';
|
||||
export const OPEN_LISTING_ERROR = 'app/ManageListingsPage/OPEN_LISTING_ERROR';
|
||||
|
||||
export const CLOSE_LISTING_REQUEST = 'app/ManageListingsPage/CLOSE_LISTING_REQUEST';
|
||||
export const CLOSE_LISTING_SUCCESS = 'app/ManageListingsPage/CLOSE_LISTING_SUCCESS';
|
||||
export const CLOSE_LISTING_ERROR = 'app/ManageListingsPage/CLOSE_LISTING_ERROR';
|
||||
|
||||
export const ADD_OWN_ENTITIES = 'app/ManageListingsPage/ADD_OWN_ENTITIES';
|
||||
|
||||
// ================ Reducer ================ //
|
||||
|
|
@ -17,6 +25,10 @@ const initialState = {
|
|||
queryListingsError: null,
|
||||
currentPageResultIds: [],
|
||||
ownEntities: {},
|
||||
openingListing: null,
|
||||
openingListingError: null,
|
||||
closingListing: null,
|
||||
closingListingError: null,
|
||||
};
|
||||
|
||||
const resultIds = data => data.data.map(l => l.id);
|
||||
|
|
@ -28,6 +40,19 @@ const merge = (state, apiResponse) => {
|
|||
};
|
||||
};
|
||||
|
||||
const updateListingAttributes = (state, listingEntity) => {
|
||||
const oldListing = state.ownEntities.listing[listingEntity.id.uuid];
|
||||
const updatedListing = { ...oldListing, attributes: listingEntity.attributes };
|
||||
const ownListingEntities = {
|
||||
...state.ownEntities.listing,
|
||||
[listingEntity.id.uuid]: updatedListing,
|
||||
};
|
||||
return {
|
||||
...state,
|
||||
ownEntities: { ...state.ownEntities, listing: ownListingEntities },
|
||||
};
|
||||
};
|
||||
|
||||
const manageListingsPageReducer = (state = initialState, action = {}) => {
|
||||
const { type, payload } = action;
|
||||
switch (type) {
|
||||
|
|
@ -51,6 +76,54 @@ const manageListingsPageReducer = (state = initialState, action = {}) => {
|
|||
console.error(payload);
|
||||
return { ...state, queryInProgress: false, queryListingsError: payload };
|
||||
|
||||
case OPEN_LISTING_REQUEST:
|
||||
return {
|
||||
...state,
|
||||
openingListing: payload.listingId,
|
||||
openingListingError: null,
|
||||
};
|
||||
case OPEN_LISTING_SUCCESS:
|
||||
return {
|
||||
...updateListingAttributes(state, payload.data),
|
||||
openingListing: null,
|
||||
};
|
||||
case OPEN_LISTING_ERROR: {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(payload);
|
||||
return {
|
||||
...state,
|
||||
openingListing: null,
|
||||
openingListingError: {
|
||||
listingId: state.openingListing,
|
||||
error: payload,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
case CLOSE_LISTING_REQUEST:
|
||||
return {
|
||||
...state,
|
||||
closingListing: payload.listingId,
|
||||
closingListingError: null,
|
||||
};
|
||||
case CLOSE_LISTING_SUCCESS:
|
||||
return {
|
||||
...updateListingAttributes(state, payload.data),
|
||||
closingListing: null,
|
||||
};
|
||||
case CLOSE_LISTING_ERROR: {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(payload);
|
||||
return {
|
||||
...state,
|
||||
closingListing: null,
|
||||
closingListingError: {
|
||||
listingId: state.closingListing,
|
||||
error: payload,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
case ADD_OWN_ENTITIES:
|
||||
return merge(state, payload);
|
||||
|
||||
|
|
@ -110,6 +183,38 @@ export const addOwnEntities = apiResponse => ({
|
|||
payload: apiResponse,
|
||||
});
|
||||
|
||||
export const openListingRequest = listingId => ({
|
||||
type: OPEN_LISTING_REQUEST,
|
||||
payload: { listingId },
|
||||
});
|
||||
|
||||
export const openListingSuccess = response => ({
|
||||
type: OPEN_LISTING_SUCCESS,
|
||||
payload: response.data,
|
||||
});
|
||||
|
||||
export const openListingError = e => ({
|
||||
type: OPEN_LISTING_ERROR,
|
||||
error: true,
|
||||
payload: e,
|
||||
});
|
||||
|
||||
export const closeListingRequest = listingId => ({
|
||||
type: CLOSE_LISTING_REQUEST,
|
||||
payload: { listingId },
|
||||
});
|
||||
|
||||
export const closeListingSuccess = response => ({
|
||||
type: CLOSE_LISTING_SUCCESS,
|
||||
payload: response.data,
|
||||
});
|
||||
|
||||
export const closeListingError = e => ({
|
||||
type: CLOSE_LISTING_ERROR,
|
||||
error: true,
|
||||
payload: e,
|
||||
});
|
||||
|
||||
export const queryListingsRequest = queryParams => ({
|
||||
type: FETCH_LISTINGS_REQUEST,
|
||||
payload: { queryParams },
|
||||
|
|
@ -126,6 +231,7 @@ export const queryListingsError = e => ({
|
|||
payload: e,
|
||||
});
|
||||
|
||||
// Throwing error for new (loadData may need that info)
|
||||
export const queryOwnListings = queryParams =>
|
||||
(dispatch, getState, sdk) => {
|
||||
dispatch(queryListingsRequest(queryParams));
|
||||
|
|
@ -145,3 +251,33 @@ export const queryOwnListings = queryParams =>
|
|||
throw e;
|
||||
});
|
||||
};
|
||||
|
||||
export const closeListing = listingId =>
|
||||
(dispatch, getState, sdk) => {
|
||||
dispatch(closeListingRequest(listingId));
|
||||
|
||||
return sdk.listings
|
||||
.close({ id: listingId }, { expand: true })
|
||||
.then(response => {
|
||||
dispatch(closeListingSuccess(response));
|
||||
return response;
|
||||
})
|
||||
.catch(e => {
|
||||
dispatch(closeListingError(e));
|
||||
});
|
||||
};
|
||||
|
||||
export const openListing = listingId =>
|
||||
(dispatch, getState, sdk) => {
|
||||
dispatch(openListingRequest(listingId));
|
||||
|
||||
return sdk.listings
|
||||
.open({ id: listingId }, { expand: true })
|
||||
.then(response => {
|
||||
dispatch(openListingSuccess(response));
|
||||
return response;
|
||||
})
|
||||
.catch(e => {
|
||||
dispatch(openListingError(e));
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { PropTypes } from 'react';
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
import { compose } from 'redux';
|
||||
import { connect } from 'react-redux';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
|
@ -9,104 +9,139 @@ import { logout, authenticationInProgress } from '../../ducks/Auth.duck';
|
|||
import { manageDisableScrolling, isScrollingDisabled } from '../../ducks/UI.duck';
|
||||
import { ManageListingCard, PageLayout, PaginationLinks, Topbar, UserNav } from '../../components';
|
||||
|
||||
import { getListingsById, queryOwnListings } from './ManageListingsPage.duck';
|
||||
import {
|
||||
closeListing,
|
||||
openListing,
|
||||
getListingsById,
|
||||
queryOwnListings,
|
||||
} from './ManageListingsPage.duck';
|
||||
import css from './ManageListingsPage.css';
|
||||
|
||||
// Pagination page size might need to be dynamic on responsive page layouts
|
||||
// Current design has max 3 columns 96 is divisible by 2 and 3
|
||||
// Current design has max 3 columns 42 is divisible by 2 and 3
|
||||
// So, there's enough cards to fill all columns on full pagination pages
|
||||
const RESULT_PAGE_SIZE = 96;
|
||||
const RESULT_PAGE_SIZE = 42;
|
||||
|
||||
export const ManageListingsPageComponent = props => {
|
||||
const {
|
||||
authInfoError,
|
||||
authInProgress,
|
||||
currentUser,
|
||||
currentUserHasListings,
|
||||
history,
|
||||
isAuthenticated,
|
||||
listings,
|
||||
location,
|
||||
logoutError,
|
||||
notificationCount,
|
||||
onLogout,
|
||||
onManageDisableScrolling,
|
||||
pagination,
|
||||
queryInProgress,
|
||||
queryListingsError,
|
||||
queryParams,
|
||||
} = props;
|
||||
export class ManageListingsPageComponent extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
const hasPaginationInfo = !!pagination && pagination.totalItems != null;
|
||||
const listingsAreLoaded = !queryInProgress && hasPaginationInfo;
|
||||
this.state = { listingMenuOpen: null };
|
||||
this.onToggleMenu = this.onToggleMenu.bind(this);
|
||||
}
|
||||
|
||||
const loadingResults = (
|
||||
<h2>
|
||||
<FormattedMessage id="ManageListingsPage.loadingOwnListings" />
|
||||
</h2>
|
||||
);
|
||||
onToggleMenu(listing) {
|
||||
this.setState({ listingMenuOpen: listing });
|
||||
}
|
||||
|
||||
const noResults = (
|
||||
<h2>
|
||||
<FormattedMessage id="ManageListingsPage.noResults" />
|
||||
</h2>
|
||||
);
|
||||
render() {
|
||||
const {
|
||||
authInfoError,
|
||||
authInProgress,
|
||||
currentUser,
|
||||
currentUserHasListings,
|
||||
history,
|
||||
isAuthenticated,
|
||||
listings,
|
||||
location,
|
||||
logoutError,
|
||||
notificationCount,
|
||||
onCloseListing,
|
||||
onOpenListing,
|
||||
onLogout,
|
||||
onManageDisableScrolling,
|
||||
pagination,
|
||||
queryInProgress,
|
||||
queryListingsError,
|
||||
queryParams,
|
||||
openingListing,
|
||||
closingListing,
|
||||
} = this.props;
|
||||
|
||||
const queryError = (
|
||||
<h2 className={css.error}>
|
||||
<FormattedMessage id="ManageListingsPage.queryError" />
|
||||
</h2>
|
||||
);
|
||||
// TODO Handle openingListingError, closingListingError,
|
||||
|
||||
const title = listingsAreLoaded
|
||||
? <h1 className={css.title}>
|
||||
<FormattedMessage
|
||||
id="ManageListingsPage.youHaveListings"
|
||||
values={{ count: pagination.totalItems }}
|
||||
const hasPaginationInfo = !!pagination && pagination.totalItems != null;
|
||||
const listingsAreLoaded = !queryInProgress && hasPaginationInfo;
|
||||
|
||||
const loadingResults = (
|
||||
<h2>
|
||||
<FormattedMessage id="ManageListingsPage.loadingOwnListings" />
|
||||
</h2>
|
||||
);
|
||||
|
||||
const queryError = (
|
||||
<h2 className={css.error}>
|
||||
<FormattedMessage id="ManageListingsPage.queryError" />
|
||||
</h2>
|
||||
);
|
||||
|
||||
const noResults = listingsAreLoaded && pagination.totalItems === 0
|
||||
? <h1 className={css.title}>
|
||||
<FormattedMessage id="ManageListingsPage.noResults" />
|
||||
</h1>
|
||||
: null;
|
||||
|
||||
const title = listingsAreLoaded && pagination.totalItems > 0
|
||||
? <h1 className={css.title}>
|
||||
<FormattedMessage
|
||||
id="ManageListingsPage.youHaveListings"
|
||||
values={{ count: pagination.totalItems }}
|
||||
/>
|
||||
</h1>
|
||||
: noResults;
|
||||
|
||||
const page = queryParams ? queryParams.page : 1;
|
||||
const paginationLinks = listingsAreLoaded && pagination && pagination.totalPages > 1
|
||||
? <PaginationLinks
|
||||
className={css.pagination}
|
||||
pageName="ManageListingsPage"
|
||||
pageSearchParams={{ page }}
|
||||
pagination={pagination}
|
||||
/>
|
||||
</h1>
|
||||
: null;
|
||||
const page = queryParams ? queryParams.page : 1;
|
||||
const paginationLinks = listingsAreLoaded && pagination && pagination.totalPages > 1
|
||||
? <PaginationLinks
|
||||
className={css.pagination}
|
||||
pageName="ManageListingsPage"
|
||||
pageSearchParams={{ page }}
|
||||
pagination={pagination}
|
||||
/>
|
||||
: null;
|
||||
: null;
|
||||
|
||||
return (
|
||||
<PageLayout authInfoError={authInfoError} logoutError={logoutError} title="Manage listings">
|
||||
<Topbar
|
||||
authInProgress={authInProgress}
|
||||
currentUser={currentUser}
|
||||
currentUserHasListings={currentUserHasListings}
|
||||
currentPage="ManageListingsPage"
|
||||
history={history}
|
||||
isAuthenticated={isAuthenticated}
|
||||
location={location}
|
||||
notificationCount={notificationCount}
|
||||
onLogout={onLogout}
|
||||
onManageDisableScrolling={onManageDisableScrolling}
|
||||
/>
|
||||
<UserNav selectedPageName="ManageListingsPage" />
|
||||
{queryInProgress ? loadingResults : null}
|
||||
{queryListingsError ? queryError : null}
|
||||
{listingsAreLoaded && pagination.totalItems === 0 ? noResults : null}
|
||||
<div className={css.listingPanel}>
|
||||
{title}
|
||||
<div className={css.listingCards}>
|
||||
{listings.map(l => (
|
||||
<ManageListingCard className={css.listingCard} key={l.id.uuid} listing={l} />
|
||||
))}
|
||||
const listingMenuOpen = this.state.listingMenuOpen;
|
||||
|
||||
return (
|
||||
<PageLayout authInfoError={authInfoError} logoutError={logoutError} title="Manage listings">
|
||||
<Topbar
|
||||
authInProgress={authInProgress}
|
||||
currentUser={currentUser}
|
||||
currentUserHasListings={currentUserHasListings}
|
||||
currentPage="ManageListingsPage"
|
||||
history={history}
|
||||
isAuthenticated={isAuthenticated}
|
||||
location={location}
|
||||
notificationCount={notificationCount}
|
||||
onLogout={onLogout}
|
||||
onManageDisableScrolling={onManageDisableScrolling}
|
||||
/>
|
||||
<UserNav selectedPageName="ManageListingsPage" />
|
||||
{queryInProgress ? loadingResults : null}
|
||||
{queryListingsError ? queryError : null}
|
||||
<div className={css.listingPanel}>
|
||||
{title}
|
||||
<div className={css.listingCards}>
|
||||
{listings.map(l => (
|
||||
<ManageListingCard
|
||||
className={css.listingCard}
|
||||
key={l.id.uuid}
|
||||
listing={l}
|
||||
isMenuOpen={!!listingMenuOpen && listingMenuOpen.id.uuid === l.id.uuid}
|
||||
actionsInProgressListingId={openingListing || closingListing}
|
||||
onToggleMenu={this.onToggleMenu}
|
||||
onCloseListing={onCloseListing}
|
||||
onOpenListing={onOpenListing}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{paginationLinks}
|
||||
</div>
|
||||
{paginationLinks}
|
||||
</div>
|
||||
|
||||
</PageLayout>
|
||||
);
|
||||
};
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ManageListingsPageComponent.defaultProps = {
|
||||
authInfoError: null,
|
||||
|
|
@ -117,9 +152,11 @@ ManageListingsPageComponent.defaultProps = {
|
|||
pagination: null,
|
||||
queryListingsError: null,
|
||||
queryParams: null,
|
||||
closingListing: null,
|
||||
openingListing: null,
|
||||
};
|
||||
|
||||
const { arrayOf, bool, func, instanceOf, number, object, shape } = PropTypes;
|
||||
const { arrayOf, bool, func, instanceOf, number, object, shape, string } = PropTypes;
|
||||
|
||||
ManageListingsPageComponent.propTypes = {
|
||||
authInfoError: instanceOf(Error),
|
||||
|
|
@ -130,12 +167,16 @@ ManageListingsPageComponent.propTypes = {
|
|||
listings: arrayOf(propTypes.ownListing),
|
||||
logoutError: instanceOf(Error),
|
||||
notificationCount: number,
|
||||
onCloseListing: func.isRequired,
|
||||
onOpenListing: func.isRequired,
|
||||
onLogout: func.isRequired,
|
||||
onManageDisableScrolling: func.isRequired,
|
||||
pagination: propTypes.pagination,
|
||||
queryInProgress: bool.isRequired,
|
||||
queryListingsError: instanceOf(Error),
|
||||
queryParams: object,
|
||||
closingListing: shape({ uuid: string.isRequired }),
|
||||
openingListing: shape({ uuid: string.isRequired }),
|
||||
|
||||
// from withRouter
|
||||
history: shape({
|
||||
|
|
@ -151,6 +192,10 @@ const mapStateToProps = state => {
|
|||
queryInProgress,
|
||||
queryListingsError,
|
||||
queryParams,
|
||||
openingListing,
|
||||
openingListingError,
|
||||
closingListing,
|
||||
closingListingError,
|
||||
} = state.ManageListingsPage;
|
||||
const listings = getListingsById(state, currentPageResultIds);
|
||||
// PageLayout needs authInfoError and logoutError, Topbar needs isAuthenticated
|
||||
|
|
@ -176,10 +221,16 @@ const mapStateToProps = state => {
|
|||
queryListingsError,
|
||||
queryParams,
|
||||
scrollingDisabled: isScrollingDisabled(state),
|
||||
openingListing,
|
||||
openingListingError,
|
||||
closingListing,
|
||||
closingListingError,
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
onCloseListing: listingId => dispatch(closeListing(listingId)),
|
||||
onOpenListing: listingId => dispatch(openListing(listingId)),
|
||||
onLogout: historyPush => dispatch(logout(historyPush)),
|
||||
onManageDisableScrolling: (componentId, disableScrolling) =>
|
||||
dispatch(manageDisableScrolling(componentId, disableScrolling)),
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@ describe('ContactDetailsPage', () => {
|
|||
isAuthenticated={false}
|
||||
onLogout={noop}
|
||||
onManageDisableScrolling={noop}
|
||||
onCloseListing={noop}
|
||||
onOpenListing={noop}
|
||||
/>
|
||||
);
|
||||
expect(tree).toMatchSnapshot();
|
||||
|
|
|
|||
|
|
@ -158,10 +158,14 @@
|
|||
"LoginForm.passwordLabel": "Password",
|
||||
"LoginForm.passwordPlaceholder": "Enter your password…",
|
||||
"LoginForm.passwordRequired": "This field is required",
|
||||
"ManageListingCard.closedListing": "This listing is closed and hidden from the marketplace.",
|
||||
"ManageListingCard.closeListing": "Close listing",
|
||||
"ManageListingCard.edit": "Edit",
|
||||
"ManageListingCard.openListing": "Open listing",
|
||||
"ManageListingCard.perNight": "per night",
|
||||
"ManageListingCard.unsupportedPrice": "({currency})",
|
||||
"ManageListingCard.unsupportedPriceTitle": "Unsupported currency ({currency})",
|
||||
"ManageListingCard.viewListing": "View listing",
|
||||
"ManageListingsPage.accountSettings": "Account settings",
|
||||
"ManageListingsPage.loadingOwnListings": "Loading listings…",
|
||||
"ManageListingsPage.noResults": "You don't have any listings.",
|
||||
|
|
|
|||
|
|
@ -6242,9 +6242,9 @@ sharetribe-scripts@0.9.2:
|
|||
optionalDependencies:
|
||||
fsevents "1.0.17"
|
||||
|
||||
"sharetribe-sdk@git+ssh://git@github.com/sharetribe/sharetribe-sdk-js#ec9fd243307ce21b95052535d2e358f681ed82b7":
|
||||
"sharetribe-sdk@git+ssh://git@github.com/sharetribe/sharetribe-sdk-js#75c5b1a9df5142a1066cd055c2157b410f4c4aee":
|
||||
version "0.0.1"
|
||||
resolved "git+ssh://git@github.com/sharetribe/sharetribe-sdk-js#ec9fd243307ce21b95052535d2e358f681ed82b7"
|
||||
resolved "git+ssh://git@github.com/sharetribe/sharetribe-sdk-js#75c5b1a9df5142a1066cd055c2157b410f4c4aee"
|
||||
dependencies:
|
||||
axios "^0.15.3"
|
||||
js-cookie "^2.1.3"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue