Migrate more docs to Flex Docs

- ci
- deploying to production
- extend listing data
- extended data (removed)
- search filters
This commit is contained in:
Kimmo Puputti 2019-01-29 11:32:07 +02:00
parent f36bc32694
commit bd9bbbed2c
20 changed files with 15 additions and 946 deletions

View file

@ -44,8 +44,9 @@ Docs.
**Note:** Remember to fork the repository before deploying the application. Connecting your own
Github repository to Heroku will make manual deploys easier.
See [documentation about deploying to Heroku](./docs/deploying-to-production.md#deploying-to-Heroku)
for more information.
See the
[How to deploy FTW to production](https://www.sharetribe.com/docs/guides/how-to-deploy-ftw-to-production/)
guide in Flex Docs for more information.
[![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy)

View file

@ -57,29 +57,25 @@ Flex Docs:
- [How to change Terms of Service and Privacy Policy in FTW](https://www.sharetribe.com/docs/guides/how-to-change-tos-and-privacy-policy-in-ftw/)
- [How to change FTW icons](https://www.sharetribe.com/docs/guides/how-to-change-ftw-icons/)
- [FTW customization checklist](https://www.sharetribe.com/docs/guides/ftw-customization-checklist/)
- [How to deploy FTW to production](https://www.sharetribe.com/docs/guides/how-to-deploy-ftw-to-production/)
See also the following articles:
- [How to extend listing data in FTW](https://www.sharetribe.com/docs/guides/how-to-extend-listing-data-in-ftw/)
- [How to change search filters in FTW](https://www.sharetribe.com/docs/guides/how-to-change-search-filters-in-ftw/)
- [How to add static pages in FTW](https://www.sharetribe.com/docs/guides/how-to-add-static-pages-in-ftw/)
- [How to test FTW](https://www.sharetribe.com/docs/guides/how-to-test-ftw/)
- [How to set up Sentry to log errors in FTW](https://www.sharetribe.com/docs/guides/how-to-set-up-sentry-to-log-errors-in-ftw/)
- [How to set up Analytics for FTW](https://www.sharetribe.com/docs/guides/how-to-set-up-analytics-for-ftw/)
- [How to set up Content Security Policy (CSP) for FTW](https://www.sharetribe.com/docs/guides/how-to-set-up-csp-for-ftw/)
- [How to use CI with FTW](https://www.sharetribe.com/docs/guides/how-to-use-ci-with-ftw/)
Extra documentation for specific topics can be found in the following files:
- [Folder structure](folder-structure.md)
- [Routing](routing.md)
- [Redux and duck files](redux.md)
- [Extended data](extended-data.md)
- See also the
[Extended data reference](https://www.sharetribe.com/docs/references/extended-data/) in Flex
Docs
- [Extend the listing data model](extend-listing.md)
- [Search filters](search-filters.md)
- [Continuous Integration (CI)](ci.md)
- [Improving performance](improving-performance.md)
- [Deploying to production](deploying-to-production.md)
- [Original create-react-app documentation](https://github.com/sharetribe/create-react-app/blob/master/packages/react-scripts/template/README.md)
The application was bootstrapped with a forked version of

View file

@ -1,66 +0,0 @@
<!DOCTYPE html>
<html lang="{{ site.lang | default: "en-US" }}">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
{% seo %}
<link rel="stylesheet" href="{{ "/assets/css/style.css?v=" | append: site.github.build_revision | relative_url }}">
<!--[if lt IE 9]>
<script src="//cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.3/html5shiv.min.js"></script>
<![endif]-->
</head>
<body>
<div class="wrapper">
<header>
<h1><a href="{{ "/" | absolute_url }}">{{ site.title | default: site.github.repository_name }}</a></h1>
{% if site.logo %}
<img src="{{site.logo | relative_url}}" alt="Logo" />
{% endif %}
<p>{{ site.description | default: site.github.project_tagline }}</p>
{% if site.github.is_project_page %}
<p class="view"><a href="{{ site.github.repository_url }}">View the Project on GitHub <small>{{ site.github.repository_nwo }}</small></a></p>
{% endif %}
{% if site.github.is_user_page %}
<p class="view"><a href="{{ site.github.owner_url }}">View My GitHub Profile</a></p>
{% endif %}
{% if site.show_downloads %}
<ul>
<li><a href="{{ site.github.zip_url }}">Download <strong>ZIP File</strong></a></li>
<li><a href="{{ site.github.tar_url }}">Download <strong>TAR Ball</strong></a></li>
<li><a href="{{ site.github.repository_url }}">View On <strong>GitHub</strong></a></li>
</ul>
{% endif %}
</header>
<aside>Navigation</aside>
<section>
{{ content }}
</section>
<footer>
{% if site.github.is_project_page %}
<p>This project is maintained by <a href="{{ site.github.owner_url }}">{{ site.github.owner_name }}</a></p>
{% endif %}
<p><small>Hosted on GitHub Pages &mdash; Theme by <a href="https://github.com/orderedlist">orderedlist</a></small></p>
</footer>
</div>
<script src="{{ "/assets/js/scale.fix.js" | relative_url }}"></script>
{% if site.google_analytics %}
<script>
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
ga('create', '{{ site.google_analytics }}', 'auto');
ga('send', 'pageview');
</script>
{% endif %}
</body>
</html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 328 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 221 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 373 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 MiB

View file

@ -1,34 +1,5 @@
# CI
The application provides a configuration to use [CircleCI](https://circleci.com/) as a continuous
integration server to run tests and other scripts. This ensures quality and can be forced to avoid
merging changes that break tests or fail audits.
Documentation moved to the Flex Docs site:
The [circle.yml](../circle.yml) file is used to configure CircleCI.
Currently the CI runs the following scripts:
## Security audit: `yarn run audit`
This command runs the security audit using [nsp](https://www.npmjs.com/package/nsp). The audit
checks for installed packages with known vulnerabilities and warns about those.
The scripts outputs information about the dependency path that added the package. If that
information is not enough, `yarn why package-name` can be used to get more detailed information
about why the package is installed.
If you are convinced that the vulnerability can be ignored, exceptions can be added to the
[.nsprc](../.nsprc) file.
## Code formatting: `yarn run format-ci`
This command fails if there are changes in the formatting that are not committed. Run
`yarn run format` to format the code and get rid of the error.
## Build: `yarn run build`
This command ensures that the build passes.
## Tests: `yarn run test-ci`
This command runs the tests.
https://www.sharetribe.com/docs/guides/how-to-use-ci-with-ftw/

View file

@ -1,142 +1,5 @@
# Deploying to production
## Getting started
Documentation moved to the Flex Docs site:
The easiest way to get started is deploying the application to Heroku. Before creating the app you
need three accounts: [Heroku](https://heroku.com/), [Stripe](https://stripe.com/) and
[MapBox](https://www.mapbox.com/). Creating the accounts is free unless you start heavily using
them.
Read more from [detailed Heroku instructions](#deploying-to-heroku) with a deploy-to-Heroku button.
In other production environments, make sure that you have [Node.js](https://nodejs.org/) and
[Yarn](https://yarnpkg.com/) installed. You will also need accounts to Stripe and MapBox.
After this you need to do the following steps:
1. [Set environment variables](#enviroment-variables)
2. [Build the app](#building-the-app)
3. [Run the node server](#starting-the-app)
### Enviroment variables
To deploy the application add at least the following variables:
| Variable | Description |
| ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `NODE_ENV` | Use value 'production' for production. |
| `PORT` | Must be set if the production environment doesn't set it by default. Not needed in Heroku. |
| `REACT_APP_SHARETRIBE_SDK_CLIENT_ID` | Client ID (API key) you can check from [Flex Console](https://flex-console.sharetribe.com/login/). You will get this from the Sharetribe team. |
| `REACT_APP_STRIPE_PUBLISHABLE_KEY` | Stripe publishable API key for generating tokens with Stripe API. It can be found from Stripe [API keys](https://dashboard.stripe.com/account/apikeys) page. Make sure to toggle _"View test data"_ on if you don't want to use real money yet. The secret key needs to be added to Flex Console. |
| `REACT_APP_MAPBOX_ACCESS_TOKEN` | Sign up for Mapbox and go to [account page](https://www.mapbox.com/account/access-tokens). Then click `Create access token`. See the Flex Docs [How to set up Mapbox for FTW](https://www.sharetribe.com/docs/guides/how-to-set-up-mapbox-for-ftw/) guide for more information. |
| `REACT_APP_CANONICAL_ROOT_URL` | Canonical root URL of the marketplace. E.g. https://the-name-of-your-app.herokuapp.com or your actual domain. This is needed for social media sharing and SEO optimization. |
There are also some other variables that can be used. See the Flex Docs
[FTW Environment configuration variables](https://www.sharetribe.com/docs/references/ftw-env/)
reference for more information.
### Building the app
Running the following command builds the app for production to the build folder. It correctly
bundles React in production mode and optimizes the build for the best performance.
`yarn build`
After this, your app is ready to be deployed.
### Starting the app
Start the server:
`yarn start`
## Deploying to Heroku
- [Forking the repository](#forking-the-repository)
- [Creating new app](#creating-new-app)
- [Connect GitHub to Heroku](#connect-github-to-heroku)
- [Heroku settings](#heroku-settings)
- [Updating enviroment variables](#updating-enviroment-variables)
- [Domains and certificates](#domains-and-certificates)
- [Heroku logs](#heroku-logs)
Before creating the app you need three accounts: [Heroku](https://heroku.com/),
[Stripe](https://stripe.com/fi) and [MapBox](https://www.mapbox.com/). Creating the accounts is free
unless you start heavily using them.
### Forking the repository
When deploying to Heroku you should use a forked repository. This makes it possible to make
[manual deploys](https://devcenter.heroku.com/articles/github-integration#manual-deploys) after
changes to environment variables.
If you haven't done this already, you just need to click the "Fork" button on the top-right corner
of the page: <img width="300" src="./assets/deploying-to-production/fork_button.png" />
For more information, see the
[How to Customize FTW](https://www.sharetribe.com/docs/guides/how-to-customize-ftw/) guide in Flex
Docs.
### Creating a new app
The easiest way is to click the _"Deploy to Heroku"_ button (check the root folder in your forked
repository) and fill in the needed information.
After deploying you will find your app from your Heroku [dashboard](https://dashboard.heroku.com/).
If you click your application you will see Overview and some other tabs.
If you don't want to use the deploy button you can also manually create a new application in Heroku
dashboard but then you have to manually set all the environment variables. If you prefer using the
command line you can also use [Heroku CLI](https://devcenter.heroku.com/articles/git).
### Connect GitHub to Heroku
Connecting your GitHub account to Heroku makes it possible to do automatic and manual deploys in the
dashboard using GitHub branches.
1. In Heroku dashboard go to _Deploy tab_ and find _Deployment method_ section.
2. Click _Connect to GitHub_ button. After that, you will see _Connect to GitHub_ section.
3. Search the repository you have forked to your account and click connect.
![Connect GitHub to Heroku](./assets/deploying-to-production/heroku-connect-git.png)
After this, you can enable automatic deploys from specific GitHub branch or do the manual deploy
from any branch in the repository.
Read more from Heroku docs:
- [GitHub Integration](https://devcenter.heroku.com/articles/github-integration)
### Heroku settings
In the _Settings tab_ you can manage your application's information, set it to maintenance mode and
delete the app if needed.
#### Updating enviroment variables
In the _Settings tab_ click _"Reveal Config Vars"_ button to see the applications environment
variables. Click edit to update them or add new ones if needed.
> **NOTE:** If you change these variables, you need to deploy the app again.
![Heroku settings](./assets/deploying-to-production/heroku-config-vars.png)
#### Domains and certificates
Heroku manages SSL certificates automatically for new applications. You can change your domain and
SSH settings in the _Settings tab_.
Read more from Heroku docs:
- [Custom Domain Names for Apps](https://devcenter.heroku.com/articles/custom-domains)
- [Manually Acquiring an SSL/TLS Certificate](https://devcenter.heroku.com/articles/acquiring-an-ssl-certificate)
![Heroku settings](./assets/deploying-to-production/heroku-domains.png)
### Heroku logs
You can find your application's logs by clicking button _"More"_ in the upper right corner and
selecting _"View logs"_ from the opening dropdown. Logs can be useful if there are problems when
deploying the app.
![Heroku logs](./assets/deploying-to-production/heroku-logs.png)
https://www.sharetribe.com/docs/guides/how-to-deploy-ftw-to-production/

View file

@ -1,475 +1,5 @@
# Extend listing data
This guide walks you through the different steps required to expand the listing data model in your
marketplace. We'll have a look on how the data is added, how it can be presented and finally how it
can be used to filter searches.
Documentation moved to the Flex Docs site:
Adding new attributes to the data model relies on a Flex concept called
[extended data](./extended-data.md).
Three main areas in extending the listing data model are:
- [Declare the attribute and it's possible values](#declare-the-attribute-and-its-possible-values)
- [Edit the listing wizard](#edit-the-listing-wizard)
- [Show the attribute on listing page](#show-the-attribute-on-listing-page)
- [Use the attribute as a search filter](#use-the-attribute-as-a-search-filter)
In this guide we will extend the listing data model by adding a _capacity_ attribute.
## Add a new attribute
### Declare the attribute and its possible values
So, you have come up with a great new extension to the listing data model, fantastic! Depending of
the type of data the new attribute is going to be used for it might make sense to define and store
the possible values for the attribute. In case the data will be free form text there might not be
use for this but in our case we wan't to fix the possible values that can be used for the capacity.
Also if the attribute will be used as a search filter it helps to keep track of the possible values
somewhere.
A commonly used way of storing the possible values for a listing attribute is to declare them in the
[marketplace-custom-config.js](../src/marketplace-custom-config.js) file. Place the values in an
array and export the variable containing the array. The exports from that file are included in the
`src/config.js` file and are available as properties in `config.custom`. Search filters and some
components used to edit and present the data rely on a data model of an array of objects that
contain `key` and `label` properties.
For the capacity attribute we'll define a set of values that describe the capacity in a few ranges
of how many people can fit into a given sauna:
```js
export const capacityOptions = [
{ key: '1to3', label: '1 to 3' },
{ key: '4to6', label: '4 to 6' },
{ key: '7to9', label: '7 to 9' },
{ key: '10plus', label: '10 plus' },
];
```
### Edit the listing wizard
Next step is to add means for modifying the attribute data in listings. This is achieved by adding
proper inputs to the `EditListingWizard`. It could probably make sense to add the input to the
_description_ tab or modify the _amenities_ tab to also include capacity but for the sake of clarity
let's add a new tab to the wizard. The new tab will be placed between the _amenities_ and _policy_
tabs.
First lets declare the tab in `EditListingWizardTab`:
```js
export const DESCRIPTION = 'description';
export const FEATURES = 'features';
export const CAPACITY = 'capacity';
export const POLICY = 'policy';
export const LOCATION = 'location';
export const PRICING = 'pricing';
export const PHOTOS = 'photos';
// EditListingWizardTab component supports these tabs
export const SUPPORTED_TABS = [DESCRIPTION, FEATURES, CAPACITY, POLICY, LOCATION, PRICING, PHOTOS];
```
Now in `EditListingWizard` we can take that tab declaration into use. Import the tab name variable
from `EditListingWizardTab` and add it to the `TABS` array.
```js
export const TABS = [DESCRIPTION, FEATURES, CAPACITY, POLICY, LOCATION, PRICING, PHOTOS];
```
Also remember to add a label for the tab in the `tabLabel` function:
```js
const tabLabel = (intl, tab) => {
let key = null;
if (tab === DESCRIPTION) {
key = 'EditListingWizard.tabLabelDescription';
} else if (tab === FEATURES) {
key = 'EditListingWizard.tabLabelFeatures';
} else if (tab === CAPACITY) {
key = 'EditListingWizard.tabLabelCapacity';
} else if (tab === POLICY) {
key = 'EditListingWizard.tabLabelPolicy';
} else if (tab === LOCATION) {
key = 'EditListingWizard.tabLabelLocation';
} else if (tab === PRICING) {
key = 'EditListingWizard.tabLabelPricing';
} else if (tab === PHOTOS) {
key = 'EditListingWizard.tabLabelPhotos';
}
return intl.formatMessage({ id: key });
};
```
The `tabCompleted` function keeps track of which data the user has already provided in order to tell
which tabs are completed. As we will be storing the capacity information in the listing's _public
data_ property (see the [extended data](./extended-data.md) documentation for more info on different
extended data types) we shall look into that property when resolving whether the capacity tab has
alerady been completed or not:
```js
/**
* Check if a wizard tab is completed.
*
* @param tab wizard's tab
* @param listing is contains some specific data if tab is completed
*
* @return true if tab / step is completed.
*/
const tabCompleted = (tab, listing) => {
const { description, geolocation, price, title, publicData } = listing.attributes;
const images = listing.images;
switch (tab) {
case DESCRIPTION:
return !!(description && title);
case FEATURES:
return !!(publicData && publicData.amenities);
case CAPACITY:
return !!(publicData && publicData.capacity);
case POLICY:
return !!(publicData && typeof publicData.rules !== 'undefined');
case LOCATION:
return !!(geolocation && publicData && publicData.location && publicData.location.address);
case PRICING:
return !!price;
case PHOTOS:
return images && images.length > 0;
default:
return false;
}
};
```
Next task is to add form and panel components that render the capacity tab. As for the form, let's
create a `EditListingCapacityForm` component:
```js
import React from 'react';
import { arrayOf, bool, func, shape, string } from 'prop-types';
import { compose } from 'redux';
import { Form as FinalForm } from 'react-final-form';
import { intlShape, injectIntl, FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import { propTypes } from '../../util/types';
import { Form, Button, FieldSelect } from '../../components';
import { required } from '../../util/validators';
import css from './EditListingCapacityForm.css';
export const EditListingCapacityFormComponent = props => (
<FinalForm
{...props}
render={fieldRenderProps => {
const {
className,
disabled,
handleSubmit,
intl,
invalid,
pristine,
saveActionMsg,
updated,
updateError,
updateInProgress,
capacityOptions,
} = fieldRenderProps;
const capacityPlaceholder = intl.formatMessage({
id: 'EditListingCapacityForm.capacityPlaceholder',
});
const errorMessage = updateError ? (
<p className={css.error}>
<FormattedMessage id="EditListingCapacityForm.updateFailed" />
</p>
) : null;
const capacityRequired = required(
intl.formatMessage({
id: 'EditListingCapacityForm.capacityRequired',
})
);
const classes = classNames(css.root, className);
const submitReady = updated && pristine;
const submitInProgress = updateInProgress;
const submitDisabled = invalid || disabled || submitInProgress;
return (
<Form className={classes} onSubmit={handleSubmit}>
{errorMessage}
<FieldSelect
className={css.capacity}
name="capacity"
id="capacity"
validate={capacityRequired}
>
<option value="">{capacityPlaceholder}</option>
{capacityOptions.map(c => (
<option key={c.key} value={c.key}>
{c.label}
</option>
))}
</FieldSelect>
<Button
className={css.submitButton}
type="submit"
inProgress={submitInProgress}
disabled={submitDisabled}
ready={submitReady}
>
{saveActionMsg}
</Button>
</Form>
);
}}
/>
);
EditListingCapacityFormComponent.defaultProps = {
selectedPlace: null,
updateError: null,
};
EditListingCapacityFormComponent.propTypes = {
intl: intlShape.isRequired,
onSubmit: func.isRequired,
saveActionMsg: string.isRequired,
updated: bool.isRequired,
updateError: propTypes.error,
updateInProgress: bool.isRequired,
capacityOptions: arrayOf(
shape({
key: string.isRequired,
label: string.isRequired,
})
).isRequired,
};
export default compose(injectIntl)(EditListingCapacityFormComponent);
```
The form component receives `capacityOptions` as a prop which are used to populate a `FieldSelect`
component for selecting the capacity. The `EditListingCapacityForm` is also added to the
`src/containers/index.js` file so that it can easilly be referenced from other components. To use
the capacity editing form we'll add a panel component which is then used in `EditListingWizardTab`
to render the wizard phase. This component we'll call `EditListingCapacityPanel`:
```js
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { FormattedMessage } from 'react-intl';
import { ensureOwnListing } from '../../util/data';
import { ListingLink } from '../../components';
import { EditListingCapacityForm } from '../../containers';
import config from '../../config.js';
import css from './EditListingCapacityPanel.css';
const EditListingCapacityPanel = props => {
const {
className,
rootClassName,
listing,
onSubmit,
onChange,
submitButtonText,
panelUpdated,
updateInProgress,
errors,
} = props;
const classes = classNames(rootClassName || css.root, className);
const currentListing = ensureOwnListing(listing);
const { publicData } = currentListing.attributes;
const panelTitle = currentListing.id ? (
<FormattedMessage
id="EditListingCapacityPanel.title"
values={{ listingTitle: <ListingLink listing={listing} /> }}
/>
) : (
<FormattedMessage id="EditListingCapacityPanel.createListingTitle" />
);
return (
<div className={classes}>
<h1 className={css.title}>{panelTitle}</h1>
<EditListingCapacityForm
className={css.form}
initialValues={{ capacity: publicData.capacity }}
onSubmit={values => {
const { capacity } = values;
const updateValues = {
publicData: {
capacity,
},
};
onSubmit(updateValues);
}}
onChange={onChange}
saveActionMsg={submitButtonText}
updated={panelUpdated}
updateError={errors.updateListingError}
updateInProgress={updateInProgress}
capacityOptions={config.custom.capacityOptions}
/>
</div>
);
};
const { func, object, string, bool } = PropTypes;
EditListingCapacityPanel.defaultProps = {
className: null,
rootClassName: null,
listing: null,
};
EditListingCapacityPanel.propTypes = {
className: string,
rootClassName: string,
// We cannot use propTypes.listing since the listing might be a draft.
listing: object,
onSubmit: func.isRequired,
onChange: func.isRequired,
submitButtonText: string.isRequired,
panelUpdated: bool.isRequired,
updateInProgress: bool.isRequired,
errors: object.isRequired,
};
export default EditListingCapacityPanel;
```
In the panel component we check for an initial capacity value from the `public_data` property of the
listing. In the submit handler the new capacity value is stored in the same property. The updated
listing object is eventually passed to the `updateListingDraft` and `requestUpdateListing` functions
in the `EditListingDetails.duck.js` file where the data updates are handled. Respectively
`EditListingCapacityPanel` needs to be exported from `src/components/index.js` for easier access
from other files.
Now that we have the panel and the form all ready we can add the panel to the `EditListingWizardTab`
component. This is done by adding a new block to the `switch` structure that handles rendering the
correct panel:
```js
case CAPACITY: {
const submitButtonTranslationKey = isNew
? 'EditListingWizard.saveNewCapacity'
: 'EditListingWizard.saveEditCapacity';
return (
<EditListingCapacityPanel
{...panelProps(CAPACITY)}
submitButtonText={intl.formatMessage({ id: submitButtonTranslationKey })}
onSubmit={values => {
onCompleteEditListingWizardTab(tab, values);
}}
/>
);
}
```
There! Now we've extended the listing data model with capacity information.
![Edit the capacity](./assets/extend-listing/edit_capacity.png)
The capacity data can now be added to new and existing listings. Next chapter describes how that
data can be presented in the listing page.
## Show the attribute on listing page
Next step in adding a new attribute to the listing is to present it in the listing page. On some
cases an extension to the listing data model can be used purely to enhance the application logic but
in our case with the capacity information we want to show the added attribute in the listing page
for potential customers.
To show the capacity attribute in the listing page, let's create a specific component for it and
place it in the `ListingPage` container. Desired outcome could also be achieved just by editing the
`ListingPage` but extracting the capacity UI parts into a separate component will simplify the
`ListingPage` and make possible upstream updates from the Flex web template repo easier as there's
less chances for merge conflicts. So, let's create a `SectionCapacity` component in the
`src/containers/ListingPage/` directory:
```js
import React from 'react';
import { array, shape, string } from 'prop-types';
import { FormattedMessage } from 'react-intl';
import css from './ListingPage.css';
const SectionCapacity = props => {
const { publicData, options } = props;
const capacity = publicData.capacity;
const capacityOption = options.find(option => option.key === capacity);
return capacityOption ? (
<div className={css.sectionCapacity}>
<h2 className={css.capacityTitle}>
<FormattedMessage id="ListingPage.capacityTitle" />
</h2>
<p className={css.capacity}>{capacityOption.label}</p>
</div>
) : null;
};
SectionCapacity.propTypes = {
options: array.isRequired,
publicData: shape({
capacity: string,
}).isRequired,
};
export default SectionCapacity;
```
Remember to add corresponding css definitions to `ListingPage.css` to get the styling right. Import
the component into `ListingPage` and place it inside the `<div className={css.mainContent}>`
element:
```js
import SectionCapacity from './SectionCapacity';
```
```js
<div className={css.mainContent}>
{/* other sections */}
<SectionCapacity publicData={publicData} options={capacityOptions} />
{/* other sections */}
</div>
```
```js
ListingPageComponent.defaultProps = {
// other default props
capacityOptions: config.custom.capacityOptions,
};
ListingPageComponent.propTypes = {
// other props
capacityOptions: array,
};
```
In the snippet above, the capacity options are passed to the `ListingPage` as a property, with the
default property value pulling the options from the custom config (`marketplace-custom-config.js`).
This way the listing page test can define it's own capacity options that are in line with test data
used in the test and custom config changes will not affect test results.
And voilà, we have listing capacity presented in the listing page!
![Capacity on listing page](./assets/extend-listing/capacity_listing_page.png)
## Use the attribute as a search filter
To see how the capacity attribute can be used to filter search results, please refer to the
[search filters documentation](./search-filters.md).
https://www.sharetribe.com/docs/guides/how-to-extend-listing-data-in-ftw/

View file

@ -1,37 +0,0 @@
# Extended Data
_Extended data_ is arbitrary information that can be attached to certain resources in the Flex API.
Extended data values can be of any valid JSON data type, including JSON data structures like object
and array. The purpose of extended data is to expand the ways in which the Flex API can be used.
## Types of extended data
Extended data is divided into two types: _public data_ and _protected data_.
### Public data
Public data can be written by anyone who has write permission to a resource and it is visible to
anyone who can read that resource.
### Protected data
Protected data, just like public data, can be modified by anyone with write access to the resource.
However, it can only be read by marketplace operators. It is also possible to reveal the protected
data at a specific point in the booking transaction process, for example if listing's address is
presented to the customer after a booking has been confirmed.
## Data schema and searched
Extended data works out of the box without any prior configuration to the marketplace. Public and
protected data can be added to listings just by passing it in the create/update request to the API.
However, optionally a _data schema_ can be defined for an extended data key. The data schema allows
that extended data attribute to be used as a search parameter in some API endpoints, like in the
listings query endpoint. Contact Sharetribe team if you need to add a schema to your extended data.
## Extended data in Flex template app
The Flex web template uses extended data by default with listing and user resources. In case of the
listings, category and amenities information is stored in the `publicData` attribute. For the users,
phone number is stored in the `protectedData` attribute. As for indexed data, the template app
relies on data schemas for categories and amenities to be configured for the marketplace in the API
as those values are used as filters in the listing search.

View file

@ -1,194 +1,5 @@
# Search filters
The search experience can be improved by adding search filters to narrow down the results. The
filters rely on listing's indexed data.
Documentation moved to the Flex Docs site:
- [Filter types](#filter-types)
- [Adding a new search filter](#adding-a-new-search-filter)
- [Common changes](#common-changes)
- [Desktop filters](#desktop-filters)
- [Desktop filters panel](#desktop-filters-panel)
- [Mobile filters](#mobile-filters)
- [Creating your own filter types](#creating-your-own-filter-types)
## Filter types
The Flex template for web has different filter types: _price_, _date range_, _select single_ and
_select multiple_. Select single and select multiple filters are generic in a way that they can be
used to filter search results using different kinds of data. The price and date range filters on the
other hand are only used for filtering by price and date range.
> NOTE: price filter should be configured from `src/marketplace-custom-config.js`. Current maximum
> value for the range is set to 1000 (USD/EUR).
Filters _select single_ and _select multiple_ can be used with extended data. The _select single_
one can be used to filter out search results with only one value per search parameter. The _select
multiple_ filters on the other hand can take multiple values for a single search parameter. These
two filter types for extended data are implemented with two different components:
- Select single filter: `SelectSingleFilter`
- Select multiple filter: `SelectMultipleFilter`
## Adding a new search filter
Next we'll guide you through the steps of adding a _capacity_ filter to the marketplace.
First step for adding a new filter is to make sure that the data being used for filtering is saved
in the listing's `publicData` attribute. On how to achieve this, please refer to the
[documentation on extending the listing data model](./extend-listing.md). Another aspect in search
filters is that the public data needs to be indexed in the API. This is currently achieved with a
manual operation done by the Sharetribe support. Once a public data attribute is added to the
listings and the data attribute is indexed, the listing searches can be filtered by that attribute
by adding a query parameter that consists of a preceding "pub\_" and the attribute name, so for the
_capacity_ attribute the parameter would be "pub_capacity".
Further reading on public data can be found in the
[extended data documentation](./extended-data.md).
**NOTE:** Please contact the Sharetribe support in order to update your listing index schema when
planning to use new listing fields as search filters.
### Common changes
A few common changes are required to add a select single or a select multiple filter to desktop and
mobile views.
First of all, the value options for the filter need to be defined. One handy place to store these is
the `marketplace-custom-config.js` file. The correct format is a list of objects with `key` and
`label` fields:
```js
export const capacityOptions = [
{
key: 'oneToThree',
label: '1 to 3',
},
{
key: 'fourToSix',
label: '4 to 6',
},
...
];
```
A few changes need to be made to the `SearchPage` container in order to get the filters to work.
`SearchPage` needs the filter options. One handy way is to add the options as a prop to the
component and then set `defaultProps` value from `config.custom` (contains the
`marketplace-custom-config.js` exports). This way tests can pass in their own values for the filter
options and filter option changes won't affect tests.
```js
SearchPageComponent.defaultProps = {
// other default props
capacityOptions: config.custom.capacityOptions,
};
SearchPageComponent.propTypes = {
// other props
capacityOptions: array,
};
```
Also a filter configuration needs to be added to the object returned by the `filters` method. The
`filters` method combines query param name and options information of each filter so that those can
be passed on to subcomponents and used for validating the filter values and rendering the filter
components.
To add a filter configuration, extract the filter options from the props and set the param name as
the one defined in your extended data indexing configuration:
```js
filters() {
const { capacityOptions } = this.props;
return {
capacityFilter: {
paramName: 'pub_capacity',
options: capacityOptions,
},
...
};
}
```
Final thing to do in `SearchPage` is to pass the filters configuration on to the components that
take care of rendering the filters. This is achieved by `primaryFilters` and `secondaryFilters`
props that are passed to `MainPanel`. The configurations are passed as an object in the same form as
the configuration object in `filters`.
```js
<MainPanel
...
primaryFilters={{
categoryFilter: filters.categoryFilter,
amenitiesFilter: filters.amenitiesFilter,
}}
secondaryFilters={{
capacityFilter: filters.capacityFilter,
}}
/>
```
These props are used for resolving filter states and validating the filter values. The contents are
passed as props to the components that render the filters: `SearchFilters`, `SearchFiltersPanel` and
`SearchFiltersMobile` .The difference between filters passed as primary and secondary varies in
mobile and desktop views:
- **Desktop:** Primary filters are shown in the top of the search view, secondary filters are
rendered in a distinct panel that opens on top of search results.
- **Mobile:** Both primary and secondary filters are rendered in the same modal.
### Desktop filters
![Desktop filters](./assets/search-filters/desktop-filters.png)
A basic desktop filter that renders as a dropdown button on top of the search results panel is
achieved using the `SelectSingleFilter` and `SelectMultipleFilter` components. To add standard
desktop filters, perform the following in `SearchFilters` component:
- declare a prop with the same name that you added the filter config to `primaryFilters`
- resolve the filters initial value with `initialValue` and `initialValues` functions
- render the filter by using a `SelectSingleFilter` or `SelectMultipleFilter` component inside the
`<div className={css.filters}>` element.
### Desktop filters panel
![Desktop filter panel](./assets/search-filters/filters-panel.png)
If more filters are required than can fit into the top filter bar, the `SearchFiltersPanel`
component can be used. It renders as a button in the top filter bar that opens a new panel that can
contain a set of filters.
To use the `SearchFiltersPanel`, do the following:
- declare a prop with the same name that you added the filter config to `secondaryFilters`
- resolve the filters initial value with `initialValue` and `initialValues` methods
- use the `SelectSingleFilter` and `SelectMultipleFilter` components inside the
`<div className={css.filtersWrapper}>` element to render the filters.
### Mobile filters
![Mobile filters](./assets/search-filters/mobile-filters.png)
The mobile view uses the same `SelectSingleFilter` and `SelectMultipleFilter` components as the
filter panel. In this case the filter components are declared in `SearchFiltersMobile`. The
following steps are required to add a mobile filter:
- declare a prop with the same name that you added the filter config to `primaryFilters` or
`secondaryFilters`
- resolve the filters initial value with `initialValue` and `initialValues` methods
- use the `SelectSingleFilter` and `SelectMultipleFilter` components inside the
`<div className={css.filtersWrapper}>` element to render the filters.
## Creating your own filter types
If you are creating new filter types note that we are using two different types of components: popup
and plain. Popup components are rendered as primary dropdowns in the search view in `SearchFilters`.
Plain components are used with `SearchFiltersMobile` and `SearchFiltersPanel`. `SearchFiltersPanel`
opens sacondary filters in a distinct panel in order to fit additional filters to the desktop search
view.
To make creating new filters easier, there are two generic components: `FilterPoup` and
`FilterPlain`. These components expect that you give form fields as child component. Check
`SelectMultipleFilter` to see how these components work.
https://www.sharetribe.com/docs/guides/how-to-change-search-filters-in-ftw/