Migrate more docs to Flex Docs
- ci - deploying to production - extend listing data - extended data (removed) - search filters
|
|
@ -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.
|
||||
|
||||
[](https://heroku.com/deploy)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 — 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>
|
||||
|
Before Width: | Height: | Size: 9.4 KiB |
|
Before Width: | Height: | Size: 108 KiB |
|
Before Width: | Height: | Size: 328 KiB |
|
Before Width: | Height: | Size: 127 KiB |
|
Before Width: | Height: | Size: 154 KiB |
|
Before Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 45 KiB |
|
Before Width: | Height: | Size: 221 KiB |
|
Before Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 373 KiB |
|
Before Width: | Height: | Size: 2.4 MiB |
33
docs/ci.md
|
|
@ -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/
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||

|
||||
|
||||
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.
|
||||
|
||||

|
||||
|
||||
#### 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 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.
|
||||
|
||||

|
||||
https://www.sharetribe.com/docs/guides/how-to-deploy-ftw-to-production/
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||

|
||||
|
||||
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!
|
||||
|
||||

|
||||
|
||||
## 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/
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
@ -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
|
||||
|
||||

|
||||
|
||||
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
|
||||
|
||||

|
||||
|
||||
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
|
||||
|
||||

|
||||
|
||||
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/
|
||||
|
|
|
|||