Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Manage Availability #972

Merged
merged 12 commits into from
Jan 7, 2019
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ way to update this template, but currently, we follow a pattern:

## Upcoming version 2019-XX-XX

- Manage availability of listings. This works for listings that have booking unit type:
'line-item/night', or 'line-item/day'. There's also 'manage availability' link in the
ManageListingCards of "your listings" page.
[#972](https://github.com/sharetribe/flex-template-web/pull/972)

## [v2.6.0] 2019-01-02

- [fix] Wrong translations for perUnit in fr.json.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
@import '../../marketplace.css';

.root {
flex-grow: 1;
width: 100%;
height: auto;
display: flex;
flex-direction: column;
padding: 11px 24px 0 24px;
}

.form {
flex-grow: 1;
}

.title {
margin-bottom: 19px;
}

@media (--viewportLarge) {
.title {
margin-bottom: 44px;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import React from 'react';
import { bool, func, object, shape, string } from 'prop-types';
import classNames from 'classnames';
import { FormattedMessage } from 'react-intl';
import { ensureOwnListing } from '../../util/data';
import { LISTING_STATE_DRAFT } from '../../util/types';
import { ListingLink } from '../../components';
import { EditListingAvailabilityForm } from '../../forms';

import css from './EditListingAvailabilityPanel.css';

const EditListingAvailabilityPanel = props => {
const {
className,
rootClassName,
listing,
availability,
onSubmit,
onChange,
submitButtonText,
panelUpdated,
updateInProgress,
errors,
} = props;

const classes = classNames(rootClassName || css.root, className);
const currentListing = ensureOwnListing(listing);
const isPublished = currentListing.id && currentListing.attributes.state !== LISTING_STATE_DRAFT;
const defaultAvailabilityPlan = {
type: 'availability-plan/day',
entries: [
{ dayOfWeek: 'mon', seats: 1 },
{ dayOfWeek: 'tue', seats: 1 },
{ dayOfWeek: 'wed', seats: 1 },
{ dayOfWeek: 'thu', seats: 1 },
{ dayOfWeek: 'fri', seats: 1 },
{ dayOfWeek: 'sat', seats: 1 },
{ dayOfWeek: 'sun', seats: 1 },
],
};
const availabilityPlan = currentListing.attributes.availabilityPlan || defaultAvailabilityPlan;

return (
<div className={classes}>
<h1 className={css.title}>
{isPublished ? (
<FormattedMessage
id="EditListingAvailabilityPanel.title"
values={{ listingTitle: <ListingLink listing={listing} /> }}
/>
) : (
<FormattedMessage id="EditListingAvailabilityPanel.createListingTitle" />
)}
</h1>
<EditListingAvailabilityForm
className={css.form}
listingId={currentListing.id}
initialValues={{ availabilityPlan }}
availability={availability}
availabilityPlan={availabilityPlan}
onSubmit={() => {
// We save the default availability plan
// I.e. this listing is available every night.
// Exceptions are handled with live edit through a calendar,
// which is visible on this panel.
onSubmit({ availabilityPlan });
}}
onChange={onChange}
saveActionMsg={submitButtonText}
updated={panelUpdated}
updateError={errors.updateListingError}
updateInProgress={updateInProgress}
/>
</div>
);
};

EditListingAvailabilityPanel.defaultProps = {
className: null,
rootClassName: null,
listing: null,
};

EditListingAvailabilityPanel.propTypes = {
className: string,
rootClassName: string,

// We cannot use propTypes.listing since the listing might be a draft.
listing: object,

availability: shape({
calendar: object.isRequired,
onFetchAvailabilityExceptions: func.isRequired,
onCreateAvailabilityException: func.isRequired,
onDeleteAvailabilityException: func.isRequired,
}).isRequired,
onSubmit: func.isRequired,
onChange: func.isRequired,
submitButtonText: string.isRequired,
panelUpdated: bool.isRequired,
updateInProgress: bool.isRequired,
errors: object.isRequired,
};

export default EditListingAvailabilityPanel;
5 changes: 4 additions & 1 deletion src/components/EditListingWizard/EditListingWizard.css
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
height: 100%;
display: flex;
flex-direction: column;

/* Content of EditListingWizard should have smaller z-index than Topbar */
z-index: 0;
}

.tabsContainer {
Expand Down Expand Up @@ -82,7 +85,7 @@
flex-grow: 1;

@media (--viewportLarge) {
padding: 90px 15vw 82px 82px;
padding: 82px 15vw 82px 82px;
border-left: 1px solid var(--matterColorNegative);
background-color: var(--matterColorLight);
}
Expand Down
16 changes: 14 additions & 2 deletions src/components/EditListingWizard/EditListingWizard.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { PayoutDetailsForm } from '../../forms';
import { Modal, NamedRedirect, Tabs } from '../../components';

import EditListingWizardTab, {
AVAILABILITY,
DESCRIPTION,
FEATURES,
POLICY,
Expand All @@ -25,7 +26,7 @@ import css from './EditListingWizard.css';

// TODO: PHOTOS panel needs to be the last one since it currently contains PayoutDetailsForm modal
// All the other panels can be reordered.
export const TABS = [DESCRIPTION, FEATURES, POLICY, LOCATION, PRICING, PHOTOS];
export const TABS = [DESCRIPTION, FEATURES, POLICY, LOCATION, PRICING, AVAILABILITY, PHOTOS];

// Tabs are horizontal in small screens
const MAX_HORIZONTAL_NAV_SCREEN_WIDTH = 1023;
Expand All @@ -42,6 +43,8 @@ const tabLabel = (intl, tab) => {
key = 'EditListingWizard.tabLabelLocation';
} else if (tab === PRICING) {
key = 'EditListingWizard.tabLabelPricing';
} else if (tab === AVAILABILITY) {
key = 'EditListingWizard.tabLabelAvailability';
} else if (tab === PHOTOS) {
key = 'EditListingWizard.tabLabelPhotos';
}
Expand All @@ -58,7 +61,14 @@ const tabLabel = (intl, tab) => {
* @return true if tab / step is completed.
*/
const tabCompleted = (tab, listing) => {
const { description, geolocation, price, title, publicData } = listing.attributes;
const {
availabilityPlan,
description,
geolocation,
price,
title,
publicData,
} = listing.attributes;
const images = listing.images;

switch (tab) {
Expand All @@ -72,6 +82,8 @@ const tabCompleted = (tab, listing) => {
return !!(geolocation && publicData && publicData.location && publicData.location.address);
case PRICING:
return !!price;
case AVAILABILITY:
return !!availabilityPlan;
case PHOTOS:
return images && images.length > 0;
default:
Expand Down
29 changes: 28 additions & 1 deletion src/components/EditListingWizard/EditListingWizardTab.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
import { ensureListing } from '../../util/data';
import { createResourceLocatorString } from '../../util/routes';
import {
EditListingAvailabilityPanel,
EditListingDescriptionPanel,
EditListingFeaturesPanel,
EditListingLocationPanel,
Expand All @@ -20,6 +21,7 @@ import {

import css from './EditListingWizard.css';

export const AVAILABILITY = 'availability';
export const DESCRIPTION = 'description';
export const FEATURES = 'features';
export const POLICY = 'policy';
Expand All @@ -28,7 +30,15 @@ export const PRICING = 'pricing';
export const PHOTOS = 'photos';

// EditListingWizardTab component supports these tabs
export const SUPPORTED_TABS = [DESCRIPTION, FEATURES, POLICY, LOCATION, PRICING, PHOTOS];
export const SUPPORTED_TABS = [
DESCRIPTION,
FEATURES,
POLICY,
LOCATION,
PRICING,
AVAILABILITY,
PHOTOS,
];

const pathParamsToNextTab = (params, tab, marketplaceTabs) => {
const nextTabIndex = marketplaceTabs.findIndex(s => s === tab) + 1;
Expand Down Expand Up @@ -71,6 +81,7 @@ const EditListingWizardTab = props => {
newListingPublished,
history,
images,
availability,
listing,
handleCreateFlowTabScrolling,
handlePublishListing,
Expand Down Expand Up @@ -213,6 +224,21 @@ const EditListingWizardTab = props => {
/>
);
}
case AVAILABILITY: {
const submitButtonTranslationKey = isNewListingFlow
? 'EditListingWizard.saveNewAvailability'
: 'EditListingWizard.saveEditAvailability';
return (
<EditListingAvailabilityPanel
{...panelProps(AVAILABILITY)}
availability={availability}
submitButtonText={intl.formatMessage({ id: submitButtonTranslationKey })}
onSubmit={values => {
onCompleteEditListingWizardTab(tab, values);
}}
/>
);
}
case PHOTOS: {
const submitButtonTranslationKey = isNewListingFlow
? 'EditListingWizard.saveNewPhotos'
Expand Down Expand Up @@ -268,6 +294,7 @@ EditListingWizardTab.propTypes = {
replace: func.isRequired,
}).isRequired,
images: array.isRequired,
availability: object.isRequired,

// We cannot use propTypes.listing since the listing might be a draft.
listing: shape({
Expand Down
10 changes: 10 additions & 0 deletions src/components/ManageListingCard/ManageListingCard.js
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,16 @@ export const ManageListingCardComponent = props => {
>
<FormattedMessage id="ManageListingCard.editListing" />
</NamedLink>

<span className={css.manageLinksSeparator}>{' • '}</span>

<NamedLink
className={css.manageLink}
name="EditListingPage"
params={{ id, slug, type: 'edit', tab: 'availability' }}
>
<FormattedMessage id="ManageListingCard.manageAvailability" />
</NamedLink>
</div>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,25 @@ exports[`ManageListingCard matches snapshot 1`] = `
values={Object {}}
/>
</NamedLink>
<span>
</span>
<NamedLink
name="EditListingPage"
params={
Object {
"id": "listing1",
"slug": "listing1-title",
"tab": "availability",
"type": "edit",
}
}
>
<FormattedMessage
id="ManageListingCard.manageAvailability"
values={Object {}}
/>
</NamedLink>
</div>
</div>
</div>
Expand Down
1 change: 1 addition & 0 deletions src/components/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export { default as Button, PrimaryButton, SecondaryButton, InlineTextButton } f
export { default as BookingPanel } from './BookingPanel/BookingPanel';
export { default as CookieConsent } from './CookieConsent/CookieConsent';
export { default as Discussion } from './Discussion/Discussion';
export { default as EditListingAvailabilityPanel } from './EditListingAvailabilityPanel/EditListingAvailabilityPanel';
export { default as EditListingDescriptionPanel } from './EditListingDescriptionPanel/EditListingDescriptionPanel';
export { default as EditListingFeaturesPanel } from './EditListingFeaturesPanel/EditListingFeaturesPanel';
export { default as EditListingLocationPanel } from './EditListingLocationPanel/EditListingLocationPanel';
Expand Down
Loading