Warning
You are viewing the technical documentation for Sharetribe. If you are looking for our no-code documentation, see our new help center.

Last updated

Add a new listing wizard tab

Learn how to add a new tab to the listing creation wizard.

Table of Contents

The template has default tabs in the listing creation wizard for both bookings and purchases. The first of those tabs, “Details” shows the attributes you have configured for your marketplace listings in Sharetribe Console. However, you can also add custom tabs to the listing creation flow.

Bike creation wizard

In addition to having a listing description, we want to allow providers to explain any potential extra features of their rental bike in more detail.

In this tutorial, you will

  • Add EditListingExtraFeaturesPanel and EditListingExtraFeaturesForm components
  • Use the new panel in EditListingWizard
  • Show the listing’s extra features on the listing page with the SectionTextMaybe component

Add EditListingExtraFeaturesPanel and EditListingExtraFeaturesForm

The different listing wizard panels can be found in the EditListingPage folder under EditListingWizard.

└── src
    └── containers
        └── EditListingPage
            └── EditListingWizard
                ├── …
                ├── EditListingAvailabilityPanel
                ├── EditListingDeliveryPanel
                ├── EditListingDetailsPanel
                ├── …

Each panel has the same structure:

  • EditListing[...]Panel.js and .module.css
  • EditListing[...]Form.js, .example.js, .test.js, and .module.css

In this tutorial, we will use the following files.

Create a new folder titled EditListingExtraFeaturesPanel in the EditListingWizard folder. Add the above files into the new folder.

└── src
    └── containers
        └── EditListingPage
            └── EditListingWizard
                └── EditListingExtraFeaturesPanel
                    ├── EditListingExtraFeaturesPanel.js
                    ├── …

If you want to add EditListingExtraFeaturesForm.example.js and EditListingExtraFeaturesForm.test.js files as well, you can download them here

This section will go through EditListingExtraFeaturesPanel in more detail.

First, we import the necessary elements used in the file. In this section, all rows start with import.

Next, we create a helper function getInitialValues to return any existing value of the extra features from the listing's public data.

const getInitialValues = params => {
  const { listing } = params;
  const { extraFeatures } = listing?.attributes.publicData || {};

  return { extraFeatures };
};

Then, we create the component itself. We first use destructuring assignment to set our props into constants for ease of use. We also create a handful of other constants to then pass to the returned element.

const EditListingExtraFeaturesPanel = props => {
  const {
    className,
    rootClassName,
    listing,
    disabled,
    ready,
    onSubmit,
    submitButtonText,
    panelUpdated,
    updateInProgress,
    errors,
  } = props;

  const classes = classNames(rootClassName || css.root, className);
  const initialValues = getInitialValues(props);
  const isPublished = listing?.id && listing?.attributes?.state !== LISTING_STATE_DRAFT;
  const unitType = listing?.attributes?.publicData?.unitType;

The second half of the component is the returned element. First, we show a title that depends on whether the listing has been published or not. Then, we show the actual EditListingExtraFeaturesForm.

In the form onSubmit function, we again use destructuring assignment for retrieving the value of extraFeatures from the incoming values, and then set extraFeatures as an attribute for publicData before calling the onSubmit function received as a prop.

  return (
    <div className={classes}>
      <H3 as="h1">
        {isPublished ? (
          <FormattedMessage
            id="EditListingExtraFeaturesPanel.title"
            values={{ listingTitle: <ListingLink listing={listing} />, lineBreak: <br /> }}
          />
        ) : (
          <FormattedMessage
            id="EditListingExtraFeaturesPanel.createListingTitle"
            values={{ lineBreak: <br /> }}
          />
        )}
      </H3>
      <EditListingExtraFeaturesForm
        className={css.form}
        initialValues={initialValues}
        onSubmit={values => {
          const { extraFeatures = '' } = values;

          // New values for listing attributes
          const updateValues = {
            publicData: {
              extraFeatures
            }
          };
          onSubmit(updateValues);
        }}
        unitType={unitType}
        saveActionMsg={submitButtonText}
        disabled={disabled}
        ready={ready}
        updated={panelUpdated}
        updateInProgress={updateInProgress}
        fetchErrors={errors}
      />
    </div>
  );
};

The rest of the file defines the necessary props more closely, and finally exports the component.

This section will go through EditListingExtraFeaturesForm in more detail.

First, we import the necessary elements used in the file. In this section, all rows start with import.

Then, we create the form component. Forms in the Sharetribe Web Template use the Final Form library for form state management. This means that on the highest level, we directly return a FinalForm component from our EditListingExtraFeaturesFormComponent. We then use the FinalForm component's render prop to customise our Extra features form behavior.

export const EditListingExtraFeaturesFormComponent = props => (
  <FinalForm
    {...props}
    render={formRenderProps => {
      const {
        formId,
        autoFocus,
        className,
        disabled,
        ready,
        handleSubmit,
        intl,
        invalid,
        pristine,
        saveActionMsg,
        updated,
        updateInProgress,
        fetchErrors,
      } = formRenderProps;

      const classes = classNames(css.root, className);
      const submitReady = (updated && pristine) || ready;
      const submitInProgress = updateInProgress;
      const submitDisabled = invalid || disabled || submitInProgress;
      const { updateListingError, showListingsError } = fetchErrors || {};

Above, we have defined the necessary constants to use in the form. It is good to note that the EditListingExtraFeaturesForm component receives an onSubmit prop, defined in EditListingExtraFeaturesPanel, that gets passed directly to the FinalForm component. The FinalForm component wraps that prop as the handleSubmit form render prop, which we then pass to the actual Form as onSubmit.

The form itself contains only one text field, and the submit button. In addition, we show any errors from props.

      return (
        <Form onSubmit={handleSubmit} className={classes}>
          {updateListingError ? (
            <p className={css.error}>
              <FormattedMessage id="EditListingExtraFeaturesForm.updateFailed" />
            </p>
          ) : null}
          {showListingsError ? (
            <p className={css.error}>
              <FormattedMessage id="EditListingExtraFeaturesForm.showListingFailed" />
            </p>
          ) : null}
          <FieldTextInput
            id={`${formId}extraFeatures`}
            name="extraFeatures"
            className={css.input}
            autoFocus={autoFocus}
            type="textarea"
            label="Extra features"
            placeholder={intl.formatMessage({ id: 'EditListingExtraFeaturesForm.extraFeaturesInputPlaceholder' })}
          />

          <Button
            className={css.submitButton}
            type="submit"
            inProgress={submitInProgress}
            disabled={submitDisabled}
            ready={submitReady}
          >
            {saveActionMsg}
          </Button>
        </Form>
      );
    }}
  />
);

The rest of the file defines the necessary props more closely, and finally exports the component. Since the form uses Console-editable marketplace texts, and therefore needs the intl object, we need to compose injectIntl when exporting the component.

export default compose(injectIntl)(
  EditListingExtraFeaturesFormComponent
);

Use the new panel in EditListingWizard

When a provider creates their listing, they navigate through the Edit Listing Wizard. The wizard has a layered structure:

  • EditListingWizardTab component imports all different listing edit panels, and determines which one to render based on a tab prop
  • EditListingWizard component imports and shows the EditListingWizardTab component and an array of supported tabs, as well as navigation and the Stripe onboarding parts of the wizard

So to use our new EditListingExtraFeaturesPanel component, we need to

  • import it in the EditListingWizardTab component
  • render it in the correct context, and
  • add EXTRAFEATURES to the list of supported tabs

Add EditListingExtraFeaturesPanel to EditListingWizardTab

First, import the EditListingExtraFeaturesPanel component in EditListingWizardTab.

└── src
    └── containers
        └── EditListingPage
            └── EditListingWizard
                ├── EditListingWizardTab.js
                ├── …
  import EditListingPricingPanel from './EditListingPricingPanel/EditListingPricingPanel';
  import EditListingPricingAndStockPanel from './EditListingPricingAndStockPanel EditListingPricingAndStockPanel';
+ import EditListingExtraFeaturesPanel from './EditListingExtraFeaturesPanel/EditListingExtraFeaturesPanel';

The EditListingWizardTab component also exports constants for all supported panels, so let’s add the new EXTRAFEATURES panel in that list, as well as into the SUPPORTED_TABS array.

export const DETAILS = 'details';
export const PRICING = 'pricing';
export const PRICING_AND_STOCK = 'pricing-and-stock';
export const EXTRAFEATURES = 'extra-features';
export const DELIVERY = 'delivery';
export const LOCATION = 'location';
export const AVAILABILITY = 'availability';
export const PHOTOS = 'photos';

// EditListingWizardTab component supports these tabs
export const SUPPORTED_TABS = [
  DETAILS,
  PRICING,
  PRICING_AND_STOCK,
  EXTRAFEATURES,
  DELIVERY,
  LOCATION,
  AVAILABILITY,
  PHOTOS,
];

The core of the EditListingWizardTab component is a switch statement that determines, based on the tab prop, which panel component to return. Add the following code block to the switch statement, before the default: row:

…
    case PHOTOS: {
      return (
        <EditListingPhotosPanel
          {...panelProps(PHOTOS)}
          listingImageConfig={config.layout.listingImage}
          images={images}
          onImageUpload={onImageUpload}
          onRemoveImage={onRemoveImage}
        />
      );
    }
    case EXTRAFEATURES: {
      return (
        <EditListingExtraFeaturesPanel
          {...panelProps(EXTRAFEATURES)}
        />
      );
    }
    default:
      return null;
  }

Show EditListingExtraFeaturesPanel in EditListingWizard

Almost there! We still need to add the EXTRAFEATURES tab handling to EditListingWizard.

└── src
    └── containers
        └── EditListingPage
            └── EditListingWizard
                ├── EditListingWizard.js
                ├── …

Let’s start by importing the relevant constant from EditListingWizardTab.

// Import modules from this directory
import EditListingWizardTab, {
  DETAILS,
  PRICING,
  PRICING_AND_STOCK,
  DELIVERY,
  EXTRAFEATURES,
  LOCATION,
  AVAILABILITY,
  PHOTOS,
} from './EditListingWizardTab';

Next, add the EXTRAFEATURES tab to the existing TABS_BOOKING array.

const TABS_DETAILS_ONLY = [DETAILS];
const TABS_PRODUCT = [DETAILS, PRICING_AND_STOCK, DELIVERY, PHOTOS];
const TABS_BOOKING = [
  DETAILS,
  LOCATION,
  PRICING,
  EXTRAFEATURES,
  AVAILABILITY,
  PHOTOS,
];
const TABS_ALL = [...TABS_PRODUCT, ...TABS_BOOKING];

The EditListingWizard component checks the tab value in two functions: tabLabelAndSubmit and tabCompleted.

The tabLabelAndSubmit function determines the marketplace text keys for the tab label and the submit button. Add the following block in the if-else sequence:

else if (tab === EXTRAFEATURES) {
    labelKey = 'EditListingWizard.tabLabelExtraFeatures';
    submitButtonKey = `EditListingWizard.${processNameString}${newOrEdit}.saveExtraFeatures`;
  }

The tabCompleted function checks whether a specific wizard tab is completed. The way it checks this is by verifying whether the listing has values in the necessary attributes.

Since the EditListingExtraFeaturesPanel is not a required attribute, we we will add a case to the switch statement and just return true whether or not it has a value. The panel saves the extra feature information in the listing’s publicData under the extraFeatures attribute, so if this was a required feature, we would check whether publicData.extraFeatures has a value.

…
    case PHOTOS:
      return images && images.length > 0;
    case EXTRAFEATURES:
      return true;
      // /** For a required attribute: **/
      // return !!publicData.extraFeatures;
    default:
      return false;

Now, if you start creating a new listing, you’ll see a new tab label in the left side navigation. However, the label only shows the relevant marketplace text key, since we have not yet added the marketplace texts in Sharetribe Console.

New tab without label

To fix this, add the following keys and values in your Console > Build > Content > Marketplace texts editor or src/translations/en.json file:

  "EditListingWizard.tabLabelExtraFeatures": "Extra features",
  "EditListingExtraFeaturesPanel.createListingTitle": "Extra features",
  "EditListingExtraFeaturesPanel.title": "Edit the extra features of {listingTitle}",
  "EditListingExtraFeaturesForm.extraFeaturesInputPlaceholder": "Explain your bike extra features...",
  "EditListingExtraFeaturesForm.updateFailed": "Updating listing failed",
  "EditListingExtraFeaturesForm.showListingFailed": "Fetching listing failed",
  "EditListingWizard.default-booking.new.savePricing": "Next: Extra features",
  "EditListingWizard.default-booking.new.saveExtraFeatures": "Next: Availability",
  "EditListingWizard.edit.saveExtraFeatures": "Save changes",
  "ListingPage.extraFeaturesTitle": "Extra features"

After adding these marketplace texts, you can create and edit the extra features of a listing. You can test the panel functionality by saving some extra features for the listing.

Bike extra features panel

When you now view the bike in your Sharetribe Console > Manage > Listings, you can see the extra features get saved in the listing's public data.

Bike extra features in Sharetribe Console

Show Extra features on listing page with SectionTextMaybe component

Now that the listing has extra features, we want to show them on the listing page. To do that, we will need to add a section to the listing page that displays the extra features. We have configured our marketplace to use the screen-wide cover photo layout, so we will modify the ListingPageCoverPhoto.js file.

└── src
    └── containers
        └── ListingPage
            ├── ListingPageCarousel.js
            ├── …

The listing pages, ListingPageCarousel and ListingPageCoverPhoto (which corresponds to the "Screen wide cover photo" layout option), show listing data using Section components, which render different types of data in a predefined way. Since the bike extra features data is free text, we can use the pre-existing SectionTextMaybe component to display the extra features.

Add the following code snippet above the SectionMapMaybe component in ListingPageCarousel:

<SectionTextMaybe
  text={publicData.extraFeatures}
  heading={intl.formatMessage({ id: 'ListingPage.extraFeaturesTitle' })}
/>

You can now see the listing extra features displayed on the listing page.

Bike extra features on listing page

Summary

In this tutorial, you

  • Added a new EditListingWizard panel, the related form, and the relevant css files
  • Edited EditListingWizardTab to support the new panel
  • Edited EditListingWizard to
    • show the correct labels and
    • check whether the listing has the necessary information related to the new panel
  • Added marketplace texts for the different contexts related to the new panel
  • Used SectionTextMaybe on the listing page for displaying the extra features