diff --git a/src/containers/ReviewForm/ReviewForm.css b/src/containers/ReviewForm/ReviewForm.css
new file mode 100644
index 00000000..c37173ca
--- /dev/null
+++ b/src/containers/ReviewForm/ReviewForm.css
@@ -0,0 +1,53 @@
+@import '../../marketplace.css';
+
+.root {
+ display: flex;
+ flex-direction: column;
+
+ /* Layout: size and positioning */
+ width: 100%;
+ height: auto;
+ margin-top: 24px;
+}
+
+.error {
+ color: var(--failColor);
+}
+
+.errorPlaceholder {
+ @media (--viewportMedium) {
+ width: 100%;
+ height: 32px;
+ }
+}
+
+.reviewRating {
+ margin-bottom: 18px;
+
+ @media (--viewportMedium) {
+ margin-bottom: 16px;
+ }
+}
+
+.reviewContent {
+ flex-shrink: 0;
+ margin-top: 24px;
+ margin-bottom: 24px;
+
+ @media (--viewportMedium) {
+ margin-top: 30px;
+ }
+}
+
+.submitButton {
+ margin-top: auto;
+ margin-bottom: 24px;
+ flex-shrink: 0;
+
+ @media (--viewportMedium) {
+ display: inline-block;
+ width: 100%;
+ margin-top: 36px;
+ margin-bottom: 0;
+ }
+}
diff --git a/src/containers/ReviewForm/ReviewForm.example.js b/src/containers/ReviewForm/ReviewForm.example.js
new file mode 100644
index 00000000..d5f7660a
--- /dev/null
+++ b/src/containers/ReviewForm/ReviewForm.example.js
@@ -0,0 +1,14 @@
+/* eslint-disable no-console */
+import ReviewForm from './ReviewForm';
+
+export const Empty = {
+ component: ReviewForm,
+ props: {
+ onSubmit: values => {
+ console.log('Submit ReviewForm with (unformatted) values:', values);
+ },
+ reviewSent: false,
+ sendReviewInProgress: false,
+ },
+ group: 'forms',
+};
diff --git a/src/containers/ReviewForm/ReviewForm.js b/src/containers/ReviewForm/ReviewForm.js
new file mode 100644
index 00000000..ea33ed66
--- /dev/null
+++ b/src/containers/ReviewForm/ReviewForm.js
@@ -0,0 +1,111 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { compose } from 'redux';
+import { reduxForm, propTypes as formPropTypes } from 'redux-form';
+import { intlShape, injectIntl, FormattedMessage } from 'react-intl';
+import classNames from 'classnames';
+import { isTransactionsTransitionAlreadyReviewed } from '../../util/errors';
+import * as propTypes from '../../util/propTypes';
+import { required } from '../../util/validators';
+import { FieldStarRating, Form, PrimaryButton, TextInputField } from '../../components';
+
+import css from './ReviewForm.css';
+
+const ReviewFormComponent = props => {
+ const {
+ className,
+ disabled,
+ handleSubmit,
+ intl,
+ form,
+ invalid,
+ submitting,
+ reviewSent,
+ sendReviewError,
+ sendReviewInProgress,
+ } = props;
+
+ const reviewRating = intl.formatMessage({ id: 'ReviewForm.reviewRatingLabel' });
+ const reviewRatingRequiredMessage = intl.formatMessage({
+ id: 'ReviewForm.reviewRatingRequired',
+ });
+
+ const reviewContent = intl.formatMessage({ id: 'ReviewForm.reviewContentLabel' });
+ const reviewContentPlaceholderMessage = intl.formatMessage({
+ id: 'ReviewForm.reviewContentPlaceholder',
+ });
+ const reviewContentRequiredMessage = intl.formatMessage({
+ id: 'ReviewForm.reviewContentRequired',
+ });
+
+ const errorMessage =
+ sendReviewError && isTransactionsTransitionAlreadyReviewed(sendReviewError) ? (
+
+
+
+ ) : (
+
+
+
+ );
+ const errorArea = sendReviewError ? errorMessage : ;
+
+ const reviewSubmitMessage = intl.formatMessage({
+ id: 'ReviewForm.reviewSubmit',
+ });
+
+ const classes = classNames(css.root, className);
+ const submitInProgress = submitting || sendReviewInProgress;
+ const submitDisabled = invalid || disabled || submitInProgress;
+
+ return (
+
+ );
+};
+
+ReviewFormComponent.defaultProps = { className: null, sendReviewError: null };
+
+const { bool, func, string } = PropTypes;
+
+ReviewFormComponent.propTypes = {
+ ...formPropTypes,
+ className: string,
+ intl: intlShape.isRequired,
+ onSubmit: func.isRequired,
+ reviewSent: bool.isRequired,
+ sendReviewError: propTypes.error,
+ sendReviewInProgress: bool.isRequired,
+};
+
+const formName = 'ReviewForm';
+
+export default compose(reduxForm({ form: formName }), injectIntl)(ReviewFormComponent);
diff --git a/src/containers/index.js b/src/containers/index.js
index 07d94cbf..b2707ce1 100644
--- a/src/containers/index.js
+++ b/src/containers/index.js
@@ -35,6 +35,7 @@ export { default as PrivacyPolicyPage } from './PrivacyPolicyPage/PrivacyPolicyP
export { default as ProfilePage } from './ProfilePage/ProfilePage';
export { default as ProfileSettingsForm } from './ProfileSettingsForm/ProfileSettingsForm';
export { default as ProfileSettingsPage } from './ProfileSettingsPage/ProfileSettingsPage';
+export { default as ReviewForm } from './ReviewForm/ReviewForm';
export { default as SalePage } from './SalePage/SalePage';
export { default as SearchPage } from './SearchPage/SearchPage';
export { default as SendMessageForm } from './SendMessageForm/SendMessageForm';
diff --git a/src/examples.js b/src/examples.js
index a9a15c4e..2a3b3452 100644
--- a/src/examples.js
+++ b/src/examples.js
@@ -59,6 +59,7 @@ import * as LoginForm from './containers/LoginForm/LoginForm.example';
import * as PasswordRecoveryForm from './containers/PasswordRecoveryForm/PasswordRecoveryForm.example';
import * as PasswordResetForm from './containers/PasswordResetForm/PasswordResetForm.example';
import * as PayoutDetailsForm from './containers/PayoutDetailsForm/PayoutDetailsForm.example';
+import * as ReviewForm from './containers/ReviewForm/ReviewForm.example';
import * as SendMessageForm from './containers/SendMessageForm/SendMessageForm.example';
import * as SignupForm from './containers/SignupForm/SignupForm.example';
import * as StripePaymentForm from './containers/StripePaymentForm/StripePaymentForm.example';
@@ -116,6 +117,7 @@ export {
PayoutDetailsForm,
ResponsiveImage,
ReviewRating,
+ ReviewForm,
SelectField,
SendMessageForm,
SignupForm,
diff --git a/src/translations/en.json b/src/translations/en.json
index d152545d..e7eba216 100644
--- a/src/translations/en.json
+++ b/src/translations/en.json
@@ -442,6 +442,17 @@
"ProfileSettingsPage.title": "Profile settings",
"ProfileSettingsPage.viewProfileLink": "View your profile",
"ResponsiveImage.noImage": "No image",
+ "ReviewForm.reviewContentLabel": "Leave a review",
+ "ReviewForm.reviewContentPlaceholder": "Descripe your experience...",
+ "ReviewForm.reviewContentRequired": "Review message is required",
+ "ReviewForm.reviewRatingLabel": "Rate your experience",
+ "ReviewForm.reviewRatingRequired": "Review message is required",
+ "ReviewForm.reviewSubmit": "Publish review",
+ "ReviewForm.reviewSubmitAlreadySent": "Review already sent. Please refresh the page.",
+ "ReviewForm.reviewSubmitFailed": "Failed to sent a review. Please try again.",
+ "ReviewModal.description": "Reviews are an important part of the Saunatime community. Please share what went well and what could have been improved.",
+ "ReviewModal.later": "Later",
+ "ReviewModal.title": "Leave a review for {revieweeName}",
"SaleDetailsPanel.acceptSaleFailed": "Oops, accepting failed. Please try again.",
"SaleDetailsPanel.activityHeading": "Activity",
"SaleDetailsPanel.bannedUserDisplayName": "Banned user",