diff --git a/src/components/FieldStarRating/FieldStarRating.css b/src/components/FieldStarRating/FieldStarRating.css new file mode 100644 index 00000000..08e000c8 --- /dev/null +++ b/src/components/FieldStarRating/FieldStarRating.css @@ -0,0 +1,51 @@ +@import '../../marketplace.css'; + +.ratingFieldSet { + border: none; + padding: 0; + margin: 0; +} + +/* fieldset can't use flexbox hence this wrapper exists */ +.rating { + display: flex; + flex-direction: row-reverse; + justify-content: flex-end; + margin-top: 16px; +} + +.rateInput { + display: none; +} + +.label { + width: 30px; +} + +.star { + fill: var(--matterColorNegative); +} + +/***** CSS Magic to Highlight Stars on Hover *****/ + +/* Star order: reverse expected (5 -> 1) and also input before label */ + +/* show actived star when checked */ +/* and actived star when hovering over a star */ +/* and show previous stars also as activated */ +.rating > .rateInput:checked ~ .label > .star, +.rating > .label:hover > .star, +.rating > .label:hover ~ .label > .star { + fill: var(--marketplaceColor); +} + +/* Darken hovered star when changing rating i.e it already is active */ +/* and darken hovered star too when changing rating (hovering inside current selection) */ +/* and darken current selection inside hovered selection */ +/* and darken hovered selection inside current selection */ +.rating > .rateInput:checked + .label:hover > .star, +.rating > .rateInput:checked ~ .label:hover > .star, +.rating > .label:hover ~ .rateInput:checked ~ .label > .star, +.rating > .rateInput:checked ~ .label:hover ~ .label > .star { + fill: var(--marketplaceColorDark); +} diff --git a/src/components/FieldStarRating/FieldStarRating.example.js b/src/components/FieldStarRating/FieldStarRating.example.js new file mode 100644 index 00000000..b84ca856 --- /dev/null +++ b/src/components/FieldStarRating/FieldStarRating.example.js @@ -0,0 +1,44 @@ +/* eslint-disable no-console */ +import React from 'react'; +import { reduxForm, propTypes as formPropTypes } from 'redux-form'; +import * as validators from '../../util/validators'; +import { Button } from '../../components'; +import FieldStarRating from './FieldStarRating'; + +const formName = 'Styleguide.FieldStarRating.Form'; + +const FormComponent = props => { + const { form, handleSubmit, invalid, pristine, submitting } = props; + const required = validators.required('This field is required'); + const submitDisabled = invalid || pristine || submitting; + + return ( +
+ + + + ); +}; + +FormComponent.propTypes = formPropTypes; + +const Form = reduxForm({ + form: formName, +})(FormComponent); + +export const StarRating = { + component: Form, + props: { + onSubmit: values => { + console.log('submit values:', values); + }, + }, + group: 'inputs', +}; diff --git a/src/components/FieldStarRating/FieldStarRating.js b/src/components/FieldStarRating/FieldStarRating.js new file mode 100644 index 00000000..d286da4f --- /dev/null +++ b/src/components/FieldStarRating/FieldStarRating.js @@ -0,0 +1,140 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { intlShape, injectIntl } from 'react-intl'; +import { Field } from 'redux-form'; +import classNames from 'classnames'; +import { IconReviewStar, ValidationError } from '../../components'; + +import css from './FieldStarRating.css'; + +class FieldStarRatingComponent extends Component { + constructor(props) { + super(props); + this.handleChange = this.handleChange.bind(this); + } + + componentWillUnmount() { + if (this.props.clearOnUnmount) { + this.props.input.onChange(''); + } + } + + handleChange(event) { + this.props.input.onChange(event.target.value); + } + + render() { + /* eslint-disable no-unused-vars */ + const { + rootClassName, + className, + inputRootClass, + clearOnUnmount, + customErrorText, + id, + intl, + label, + input, + meta, + ...rest + } = this.props; + /* eslint-enable no-unused-vars */ + + const { touched, error } = meta; + const errorText = customErrorText || error; + const fieldMeta = { touched, error: errorText }; + + const { value, ...restInputProps } = input; + const inputProps = { ...restInputProps, type: 'radio', name: 'rating', ...rest }; + + const classes = classNames(rootClassName || css.root, className); + + const createStarRating = starCount => { + let inputsAndLabels = []; + + // Star inpu order: reverse order expected (5 -> 1) and also input before label + // This is due to CSS selectors. + // Sibling combinator (~) selects following siblings, but we want to select previous siblings + for (let i = starCount; i > 0; i--) { + const inputValue = `${i}`; + const starId = `star${i}`; + const inputId = `${id}.${starId}`; + + inputsAndLabels.push( + + ); + + inputsAndLabels.push( + + ); + } + return inputsAndLabels; + }; + + return ( +
+
{ + this.ratingFieldSet = c; + }} + > + {label ? {label} : null} +
{createStarRating(5)}
+
+ +
+ ); + } +} + +FieldStarRatingComponent.defaultProps = { + rootClassName: null, + className: null, + clearOnUnmount: false, + customErrorText: null, + label: null, +}; + +const { string, bool, shape, func, object } = PropTypes; + +FieldStarRatingComponent.propTypes = { + rootClassName: string, + className: string, + clearOnUnmount: bool, + id: string.isRequired, + label: string, + + // Error message that can be manually passed to input field, + // overrides default validation message + customErrorText: string, + + // Generated by redux-form's Field component + input: shape({ + onChange: func.isRequired, + }).isRequired, + meta: object.isRequired, + + // from injectIntl + intl: intlShape.isRequired, +}; + +const FieldStarRating = props => { + return ; +}; + +export default injectIntl(FieldStarRating); diff --git a/src/components/index.js b/src/components/index.js index 7cd94ac0..0f73a6ad 100644 --- a/src/components/index.js +++ b/src/components/index.js @@ -27,6 +27,7 @@ export { default as EditListingWizard } from './EditListingWizard/EditListingWiz export { default as ExpandingTextarea } from './ExpandingTextarea/ExpandingTextarea'; export { default as ExternalLink } from './ExternalLink/ExternalLink'; export { default as FilterPanel } from './FilterPanel/FilterPanel'; +export { default as FieldStarRating } from './FieldStarRating/FieldStarRating'; export { default as Footer } from './Footer/Footer'; export { default as Form } from './Form/Form'; export { default as IconBannedUser } from './IconBannedUser/IconBannedUser'; diff --git a/src/examples.js b/src/examples.js index 8bd4ca94..a9a15c4e 100644 --- a/src/examples.js +++ b/src/examples.js @@ -10,6 +10,7 @@ import * as DateInputField from './components/DateInputField/DateInputField.exam import * as DateRangeInputField from './components/DateRangeInputField/DateRangeInputField.example'; import * as EditListingWizard from './components/EditListingWizard/EditListingWizard.example'; import * as ExpandingTextarea from './components/ExpandingTextarea/ExpandingTextarea.example'; +import * as FieldStarRating from './components/FieldStarRating/FieldStarRating.example'; import * as Footer from './components/Footer/Footer.example'; import * as IconBannedUser from './components/IconBannedUser/IconBannedUser.example'; import * as IconCheckmark from './components/IconCheckmark/IconCheckmark.example'; @@ -82,6 +83,7 @@ export { EditListingWizard, EmailVerificationForm, ExpandingTextarea, + FieldStarRating, Footer, IconBannedUser, IconCheckmark, diff --git a/src/marketplaceIndex.css b/src/marketplaceIndex.css index f4ea3fd1..3f0be487 100644 --- a/src/marketplaceIndex.css +++ b/src/marketplaceIndex.css @@ -71,6 +71,7 @@ ul { padding: 0; } +legend, label { @apply --marketplaceH4FontStyles; font-weight: var(--fontWeightSemiBold); diff --git a/src/translations/en.json b/src/translations/en.json index f3d08fa3..d152545d 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -169,6 +169,11 @@ "EmailVerificationForm.verifying": "Verifying…", "EmailVerificationPage.loadingUserInformation": "Loading user information…", "EmailVerificationPage.title": "Verify your email address", + "FieldStarRating.star1": "Bad experience - 1 star", + "FieldStarRating.star2": "Not so nice - 2 stars", + "FieldStarRating.star3": "OK - 3 stars", + "FieldStarRating.star4": "Good - 4 stars", + "FieldStarRating.star5": "Awesome - 5 stars", "Footer.copyright": "© Sharetribe 2017", "Footer.goToFacebook": "Go to Facebook page", "Footer.goToInstagram": "Go to Instagram page",