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

Seats: availability management, booking flow, search filter #502

Open
wants to merge 40 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
3317c96
Rename files: AvailabilityModeSelector > AvailabilitySingleSeatSelector
Gnito Oct 31, 2024
377bdca
AvailabilitySingleSeatSelector: refactor
Gnito Oct 31, 2024
6786908
AvailabilitySingleSeatSelector: use conditionally
Gnito Nov 1, 2024
bb2b018
Add FieldSeatsInput component
Gnito Nov 4, 2024
2dfe548
EditListingAvailabilityExceptionForm: take AvailabilityMultipleSeatsI…
Gnito Nov 4, 2024
ed2eef3
EditListingAvailabilityPanel: rename some plan-related variables
Gnito Nov 11, 2024
0eb4813
AvailabilityPlanEntries: refactor & add seats input if 'useMultipleSe…
Gnito Nov 11, 2024
675e1ec
EditListingAvailabilityPlanForm: pass 'useMultipleSeats' and 'unitType'
Gnito Nov 11, 2024
0126e82
EditListingAvailabilityPanel: use seats with AvailabilityPlanForm
Gnito Nov 11, 2024
8acaa3c
AvailabilityPlanEntries: remove unused param (intl)
Gnito Nov 12, 2024
3e40a2a
ExceptionDateTimeRange: improve icon styles
Gnito Nov 12, 2024
021eb0d
marketplaceDefaults.css: do not use box-shadow with select (consistency)
Gnito Nov 12, 2024
fa26820
TimeRange: refactor (use useIntl)
Gnito Nov 12, 2024
513689f
WeeklyCalendar: change UI and add support for multiple seats
Gnito Nov 12, 2024
e0dc0fe
EditListingAvailabilityExceptionForm: improve JsDocs
Gnito Nov 14, 2024
ca1b770
ExceptionDateRange: improve JsDocs
Gnito Nov 14, 2024
b1ce715
ExceptionDateTimeRange: improve JsDocs
Gnito Nov 14, 2024
eb1a941
EditListingAvailabilityPlanForm: improve JsDocs
Gnito Nov 14, 2024
a96dc4b
FieldTimeZoneSelect: improve JsDocs
Gnito Nov 14, 2024
e2dc907
WeekPicker: improve JsDocs
Gnito Nov 14, 2024
f98315a
EditListingAvailabilityPanel: improve JsDocs and styles
Gnito Nov 14, 2024
7a92a00
configHelpers: add small comment about availabilityType
Gnito Nov 14, 2024
b1f11b8
listingConfig: add comment about availabilityType
Gnito Nov 14, 2024
3898b0d
EditListingPage: update tests
Gnito Nov 14, 2024
11dd6d3
fix: copy-paste failure. input font-size must be >=16px
Gnito Nov 15, 2024
2088898
en.json: update marketplace texts
Gnito Nov 28, 2024
1434c43
AvailabilityPlanEntries: default should be 1
Gnito Dec 12, 2024
91cbfd4
EditListingWizard: remove withViewport HOC. Android seems to send res…
Gnito Dec 16, 2024
b647815
Ensure scrollIntoView exists (fixes test)
Gnito Dec 17, 2024
80b646f
add seats functionality to date picker
shareoc Nov 14, 2024
a964387
adjust maximum amount of seats and refactor
shareoc Nov 18, 2024
a71eaa5
refactor seats calculation
shareoc Nov 25, 2024
ec1ba39
fix bug with inquiries and inbox page
shareoc Nov 28, 2024
a6d7827
add seats parameters to transactionPage.js
shareoc Nov 28, 2024
62c36a4
FieldDateAndTimeInput.example.js: fix missing props
Gnito Dec 18, 2024
635dc09
Merge pull request #506 from sharetribe/customer-seats
Gnito Dec 20, 2024
e044d48
Add NumberInput component
SariSaar Nov 25, 2024
cebf575
Add seats filter component and functionality
SariSaar Dec 2, 2024
7edd20b
Update disabled state
Gnito Dec 20, 2024
33cd907
Merge pull request #514 from sharetribe/filter-seats
Gnito Dec 20, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 33 additions & 5 deletions server/api-util/lineItems.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,14 @@ const getHourQuantityAndLineItems = orderData => {
return { quantity, extraLineItems: [] };
};

const getHoursWithSeatsAndLineItems = orderData => {
const { bookingStart, bookingEnd, seats } = orderData || {};
const units =
bookingStart && bookingEnd ? calculateQuantityFromHours(bookingStart, bookingEnd) : null;

return { units, seats, extraLineItems: [] };
};

/**
* Calculate quantity based on days or nights between given bookingDates.
*
Expand All @@ -87,6 +95,13 @@ const getDateRangeQuantityAndLineItems = (orderData, code) => {
return { quantity, extraLineItems: [] };
};

const getDateRangeWithSeatsAndLineItems = (orderData, code) => {
const { bookingStart, bookingEnd, seats } = orderData;
const units =
bookingStart && bookingEnd ? calculateQuantityFromDates(bookingStart, bookingEnd, code) : null;
return { units, seats, extraLineItems: [] };
};

/**
* Returns collection of lineItems (max 50)
*
Expand Down Expand Up @@ -140,18 +155,30 @@ exports.transactionLineItems = (listing, orderData, providerCommission, customer
const quantityAndExtraLineItems =
unitType === 'item'
? getItemQuantityAndLineItems(orderData, publicData, currency)
: unitType === 'hour' && orderData.seats
? getHoursWithSeatsAndLineItems(orderData)
: unitType === 'hour'
? getHourQuantityAndLineItems(orderData)
: ['day', 'night'].includes(unitType) && orderData.seats
? getDateRangeWithSeatsAndLineItems(orderData, code)
: ['day', 'night'].includes(unitType)
? getDateRangeQuantityAndLineItems(orderData, code)
: {};

const { quantity, extraLineItems } = quantityAndExtraLineItems;
const { quantity, units, seats, extraLineItems } = quantityAndExtraLineItems;

// Throw error if there is no quantity information given
if (!quantity) {
const message = `Error: transition should contain quantity information:
stockReservationQuantity, quantity, or bookingStart & bookingEnd (if "line-item/day" or "line-item/night" is used)`;
if (!quantity && !(units && seats)) {
const missingFields = [];

if (!quantity) missingFields.push('quantity');
if (!units) missingFields.push('units');
if (!seats) missingFields.push('seats');

const message = `Error: orderData is missing the following information: ${missingFields.join(
', '
)}. Quantity or either units & seats is required.`;

const error = new Error(message);
error.status = 400;
error.statusText = message;
Expand All @@ -169,10 +196,11 @@ exports.transactionLineItems = (listing, orderData, providerCommission, customer
*
* By default OrderBreakdown prints line items inside LineItemUnknownItemsMaybe if the lineItem code is not recognized. */

const quantityOrSeats = !!units && !!seats ? { units, seats } : { quantity };
const order = {
code,
unitPrice,
quantity,
...quantityOrSeats,
includeFor: ['customer', 'provider'],
};

Expand Down
56 changes: 56 additions & 0 deletions src/components/FieldNumber/FieldNumber.example.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import React from 'react';
import { Form as FinalForm, FormSpy } from 'react-final-form';
import { Button } from '../../components';
import FieldNumber from './FieldNumber';

const formName = 'Styleguide.FieldNumber.Form';

const FormComponent = props => (
<FinalForm
{...props}
formId={formName}
render={fieldRenderProps => {
const { form, handleSubmit, onChange, invalid, pristine, submitting } = fieldRenderProps;

const submitDisabled = invalid || pristine || submitting;

return (
<form
onSubmit={e => {
e.preventDefault();
handleSubmit(e);
}}
>
<FormSpy onChange={onChange} subscription={{ values: true, dirty: true }} />
<FieldNumber
id="number-id1"
name="number-id1"
component="input"
label="Select number between 1 and 20"
minValue={1}
maxValue={20}
/>

<Button style={{ marginTop: 24 }} type="submit" disabled={submitDisabled}>
Submit
</Button>
</form>
);
}}
/>
);

export const Number = {
component: FormComponent,
props: {
onChange: formState => {
if (Object.keys(formState.values).length > 0) {
console.log('form values changed to:', formState.values);
}
},
onSubmit: values => {
console.log('Submit values of FieldNumber: ', values);
},
},
group: 'inputs',
};
156 changes: 156 additions & 0 deletions src/components/FieldNumber/FieldNumber.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import React from 'react';
import classNames from 'classnames';
import { Field } from 'react-final-form';
import { injectIntl } from '../../util/reactIntl';

import css from './FieldNumber.module.css';

const decrement = 'decrement';
const increment = 'increment';

const getIconClasses = props => {
const { className, disabled } = props;
const classes = classNames(className, css.iconContainer, { [css.disabled]: disabled });
const iconClassName = classNames(css.icon, { [css.disabled]: disabled });

return {
classes,
iconClassName,
};
};

const IconMinus = props => {
const { classes, iconClassName } = getIconClasses(props);

return (
<svg
className={classes}
width="40"
height="40"
viewBox="0 0 40 40"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<rect x="0.5" y="0.5" width="39" height="39" rx="6" />
<path
className={iconClassName}
fillRule="evenodd"
clipRule="evenodd"
d="M11.5 20C11.5 19.4477 11.9477 19 12.5 19H27.5C28.0523 19 28.5 19.4477 28.5 20C28.5 20.5523 28.0523 21 27.5 21H12.5C11.9477 21 11.5 20.5523 11.5 20Z"
/>
</svg>
);
};

const IconPlus = props => {
const { classes, iconClassName } = getIconClasses(props);

return (
<svg
className={classes}
width="40"
height="40"
viewBox="0 0 40 40"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<rect x="0.5" y="0.5" width="39" height="39" rx="6" />
<path
className={iconClassName}
fillRule="evenodd"
clipRule="evenodd"
d="M20 11.5C20.5523 11.5 21 11.9477 21 12.5V19.0001H27.5C28.0523 19.0001 28.5 19.4478 28.5 20.0001C28.5 20.5524 28.0523 21.0001 27.5 21.0001H21V27.5C21 28.0523 20.5523 28.5 20 28.5C19.4477 28.5 19 28.0523 19 27.5V21.0001H12.5C11.9477 21.0001 11.5 20.5524 11.5 20.0001C11.5 19.4478 11.9477 19.0001 12.5 19.0001H19V12.5C19 11.9477 19.4477 11.5 20 11.5Z"
/>
</svg>
);
};

const NumberInputComponent = props => {
const { value: rawValue, onChange } = props.input;
const { initialValue, minValue = 0, maxValue, svgClassName, intl } = props;
const value =
rawValue && rawValue >= minValue
? Number.parseInt(rawValue)
: initialValue && initialValue >= minValue
? Number.parseInt(initialValue)
: minValue;

const handleValueChange = event => {
const { name } = event.target;
if (name === increment) {
onChange(value + 1);
} else if (name === decrement) {
onChange(value - 1);
}
};

return (
<span className={css.numberInputWrapper}>
<button
className={css.numberButton}
name={decrement}
type="button"
onClick={handleValueChange}
disabled={value <= minValue}
title={intl.formatMessage({ id: 'NumberInput.decrementButton' })}
>
<IconMinus disabled={value <= minValue} className={svgClassName} />
</button>
<span className={css.numberInput}>{value}</span>
<button
className={css.numberButton}
name={increment}
type="button"
onClick={handleValueChange}
disabled={value >= maxValue}
title={intl.formatMessage({ id: 'NumberInput.incrementButton' })}
>
<IconPlus disabled={value >= maxValue} className={svgClassName} />
</button>
</span>
);
};

const NumberInput = injectIntl(NumberInputComponent);

/**
* Renders a numeric selector with - and + icons
* @component
* @param {Object} props
* @param {string} props.id
* @param {string} props.name
* @param {string?} props.className
* @param {string?} props.rootClassName
* @param {string?} props.svgClassName
* @param {string?} props.textClassName
* @param {string?} props.label
* @param {number?} props.maxValue
* @param {number?} props.minValue
* @returns {JSX.Element} containing a numeric selector field that can be used in a form
*/
const FieldNumberComponent = props => {
const { rootClassName, className, textClassName, id, name, label } = props;

const classes = classNames(rootClassName || css.root, className);

return (
<span className={classes}>
{label ? (
<label htmlFor={id} className={textClassName}>
{label}
</label>
) : null}
<Field name={name}>
{fieldRenderProps => {
return (
<div>
<NumberInput {...props} {...fieldRenderProps} />
</div>
);
}}
</Field>
</span>
);
};

export default FieldNumberComponent;
91 changes: 91 additions & 0 deletions src/components/FieldNumber/FieldNumber.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
@import '../../styles/customMediaQueries.css';

.root {
position: relative;
}

.numberInputWrapper {
align-self: baseline;
height: 40px;

display: flex;
align-items: center;
justify-content: space-between;
cursor: pointer;
min-width: 104px;
}

.numberButton {
stroke-width: 1px;
border-radius: 10%;
fill: var(--colorSecondaryButton);
height: 40px;
width: 40px;
cursor: pointer;
display: flex;
padding: 0px;
align-items: center;
justify-content: center;
border: none;

svg {
pointer-events: none;
}
}

.numberButton:hover {
.iconContainer {
stroke: #b8bfd1;
}
}

.numberButton:focus {
outline: none;

.iconContainer {
stroke: #b8bfd1;
}
}
.numberButton:active {
.iconContainer {
fill: var(--colorGrey50);
stroke: #b8bfd1;
}
}
.numberButton:disabled {
cursor: not-allowed;

.disabled.iconContainer {
fill: var(--colorSecondaryButton);
stroke: #d8dce6;
}
}

.numberInput {
width: 60px;
text-align: center;
padding: 0px 10px;
cursor: default;
font-size: 16px;
font-weight: var(--fontWeightMedium);
}

.iconContainer {
fill: var(--colorWhite);
/* stroke color set to match the color of the input
borders defined in marketplaceDefaults.css */
stroke: #d8dce6;
stroke-width: 1px;
}

.icon {
stroke: var(--colorGrey700);
fill: var(--colorGrey700);
}

.disabled {
.icon {
stroke: var(--colorGrey200);
fill: var(--colorGrey200);
}
}
Loading