Merge pull request #198 from sharetribe/input-styles

Location search Input style
This commit is contained in:
Kimmo Puputti 2017-06-02 12:06:07 +03:00 committed by GitHub
commit 679f9203e6
21 changed files with 474 additions and 121 deletions

View file

@ -1,19 +1,5 @@
.root {
display: block;
/* Font */
font-size: 16px;
letter-spacing: -0.4px;
/* Border */
border: 1px solid #979797;
border-radius: 0px; /* Remove default iOS border radius */
/* Dimensions */
padding-left: 18px;
padding-right: 18px;
height: 50px;
width: 100%;
}
.inline {

View file

@ -1,31 +1,96 @@
@import '../../marketplace.css';
:root {
--inputHeight: 50px;
--sidePadding: 24px;
--sidePaddingDesktop: 36px;
--poweredImageHeight: 18px;
}
.root {
position: relative;
display: flex;
}
.icon {
display: flex;
width: 24px;
align-self: stretch;
border-bottom: 3px solid var(--marketplaceColor);
background-color: var(--matterColorLight);
}
.iconSvg {
margin: auto;
}
.iconSvgGroup {
stroke: var(--marketplaceColor);
}
.input {
height: 50px;
flex-grow: 1;
height: var(--inputHeight);
padding-left: 18px;
}
.predictions {
/*
Predictions container can be overriden with new container styles for
size and position, etc.
*/
.predictionsRoot {
position: absolute;
margin: 0;
top: 50px;
width: 100%;
background-color: #fff;
border: 1px solid #eee;
border-top: none;
padding-bottom: 98px;
top: var(--inputHeight);
left: 0;
background-color: var(--marketplaceColor);
border-bottom-left-radius: var(--borderRadius);
border-bottom-right-radius: var(--borderRadius);
box-shadow: var(--boxShadowPopup);
z-index: var(--zIndexPopup + 1);
}
/*
The Google Maps API TOS requires us to show a Powered by Google logo
next to the autocomplete service predictions. It is rendered to the
bottom of the container.
*/
.poweredByGoogle {
position: absolute;
bottom: 27px;
width: 100%;
height: var(--poweredImageHeight);
background-image: url(./images/powered_by_google_on_non_white_hdpi.png);
background-size: auto var(--poweredImageHeight);
background-position: center;
@media (--desktopViewport) {
background-position: center left var(--sidePaddingDesktop);
}
}
/* List of predictions, with a responsive padding size */
.predictions {
composes: searchResultsFont from '../../marketplace.css';
margin: 0;
padding: 14px 0;
& li {
padding: 0.5rem 1rem;
border-bottom: 1px solid #eee;
color: var(--matterColorLight);
&:last-child {
border-bottom: none;
/* Assign enough vertical padding to make the element at least 44px high */
padding: 10px var(--sidePadding);
@media (--desktopViewport) {
padding: 10px var(--sidePaddingDesktop);
}
&:hover,
&.highlighted {
background-color: #eee;
text-decoration: underline;
cursor: pointer;
}
}
}

View file

@ -10,7 +10,9 @@ const FormComponent = props => {
<form onSubmit={handleSubmit}>
<label htmlFor="location">Select location:</label>
<Field name="location" format={null} component={LocationAutocompleteInput} />
<Button type="submit" disabled={pristine || submitting}>Submit</Button>
<Button type="submit" style={{ marginTop: '24px' }} disabled={pristine || submitting}>
Submit
</Button>
</form>
);
};

View file

@ -1,7 +1,6 @@
import React, { Component, PropTypes } from 'react';
import { debounce } from 'lodash';
import classNames from 'classnames';
import { Input } from '../../components';
import * as propTypes from '../../util/propTypes';
import { getPlacePredictions, getPlaceDetails } from '../../util/googleMaps';
@ -15,9 +14,38 @@ const KEY_CODE_TAB = 9;
const DIRECTION_UP = 'up';
const DIRECTION_DOWN = 'down';
const Icon = () => (
<svg
className={css.iconSvg}
width="21"
height="22"
viewBox="0 0 21 22"
xmlns="http://www.w3.org/2000/svg"
>
<g
className={css.iconSvgGroup}
transform="matrix(-1 0 0 1 20 1)"
strokeWidth="2"
fill="none"
fillRule="evenodd"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M13 14l5.241 5.241" /><circle cx="7.5" cy="7.5" r="7.5" />
</g>
</svg>
);
// Renders the autocompletion prediction results in a list
const LocationPredictionsList = props => {
const { predictions, highlightedIndex, onSelectStart, onSelectEnd } = props;
const {
rootClassName,
className,
predictions,
highlightedIndex,
onSelectStart,
onSelectEnd,
} = props;
if (predictions.length === 0) {
return null;
}
@ -41,18 +69,29 @@ const LocationPredictionsList = props => {
};
/* eslint-enable jsx-a11y/no-static-element-interactions */
const classes = classNames(rootClassName || css.predictionsRoot, className);
return (
<ul className={css.predictions}>
{predictions.map(item)}
</ul>
<div className={classes}>
<ul className={css.predictions}>
{predictions.map(item)}
</ul>
<div className={css.poweredByGoogle} />
</div>
);
};
const { bool, shape, string, arrayOf, func, any, number } = PropTypes;
LocationPredictionsList.defaultProps = { highlightedIndex: null };
LocationPredictionsList.defaultProps = {
rootClassName: null,
className: null,
highlightedIndex: null,
};
LocationPredictionsList.propTypes = {
rootClassName: string,
className: string,
predictions: arrayOf(
shape({
id: string.isRequired,
@ -299,7 +338,16 @@ class LocationAutocompleteInput extends Component {
}
render() {
const { autoFocus, className, placeholder, input } = this.props;
const {
autoFocus,
rootClassName,
className,
iconClassName,
inputClassName,
predictionsClassName,
placeholder,
input,
} = this.props;
const { name, onFocus } = input;
const { search, predictions } = currentValue(this.props);
@ -308,17 +356,24 @@ class LocationAutocompleteInput extends Component {
onFocus(e);
};
const rootClass = classNames(rootClassName || css.root, className);
const iconClass = classNames(css.icon, iconClassName);
const inputClass = classNames(css.input, inputClassName);
const predictionsClass = classNames(predictionsClassName);
// Only render predictions when the input has focus. For
// development and easier workflow with the browser devtools, you
// might want to hardcode this to `true`. Otherwise the dropdown
// list will disappear.
//
const renderPredictions = this.state.inputHasFocus;
return (
<div className={css.root}>
<Input
className={classNames(css.input, className)}
<div className={rootClass}>
<div className={iconClass}>
<Icon />
</div>
<input
className={inputClass}
type="search"
autoComplete="off"
autoFocus={autoFocus}
@ -332,6 +387,7 @@ class LocationAutocompleteInput extends Component {
/>
{renderPredictions
? <LocationPredictionsList
className={predictionsClass}
predictions={predictions}
highlightedIndex={this.state.highlightedIndex}
onSelectStart={this.handlePredictionsSelectStart}
@ -343,11 +399,23 @@ class LocationAutocompleteInput extends Component {
}
}
LocationAutocompleteInput.defaultProps = { autoFocus: false, className: '', placeholder: '' };
LocationAutocompleteInput.defaultProps = {
autoFocus: false,
rootClassName: null,
className: null,
iconClassName: null,
inputClassName: null,
predictionsClassName: null,
placeholder: '',
};
LocationAutocompleteInput.propTypes = {
autoFocus: bool,
rootClassName: string,
className: string,
iconClassName: string,
inputClassName: string,
predictionsClassName: string,
placeholder: string,
input: shape({
name: string.isRequired,

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

View file

@ -10,7 +10,7 @@
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: baseline;
align-items: flex-start;
padding: 0 36px;
/* fill */
@ -32,7 +32,6 @@
/* logo */
.logoLink {
align-self: flex-start;
flex-shrink: 0;
display: flex;
align-items: center;
@ -46,10 +45,8 @@
}
/* Search */
/* TODO This is placeholder, it needs Search component */
.searchLink {
min-width: 220px;
align-self: flex-start;
flex-grow: 1;
height: 100%;
margin-right: 24px;
}
@ -60,15 +57,9 @@
color: var(--matterColor);
}
/* Spacer */
/* Extra space is gathered between search and createListingLink */
.spacer {
flex-grow: 1;
}
/* Create listing (CTA for providers) */
.createListingLink {
align-self: flex-start;
flex-shrink: 0;
height: 100%;
margin-right: 24px;
}
@ -81,7 +72,6 @@
/* Inbox */
.inboxLink {
align-self: flex-start;
height: 100%;
margin-right: 24px;
}
@ -94,7 +84,6 @@
/* Profile menu */
.profileMenuLabel {
align-self: flex-start;
flex-shrink: 0;
display: flex;
align-items: center;
@ -112,13 +101,13 @@
/* Authentication */
.signupLink {
align-self: flex-start;
flex-shrink: 0;
height: 100%;
margin-right: 24px;
}
.loginLink {
align-self: flex-start;
flex-shrink: 0;
height: 100%;
}

View file

@ -1,3 +1,4 @@
/* eslint-disable no-console */
import { fakeIntl } from '../../util/test-data';
import TopbarDesktop from './TopbarDesktop';
@ -9,6 +10,9 @@ export const AuthenticatedDesktopTopbar = {
isAuthenticated: true,
currentUserHasListings: true,
name: 'John Doe',
onSearchSubmit: values => {
console.log('submit search:', values);
},
intl: fakeIntl,
onLogout: noop,
},

View file

@ -1,5 +1,5 @@
import React, { PropTypes } from 'react';
import { FormattedMessage, intlShape, injectIntl } from 'react-intl';
import { FormattedMessage, intlShape } from 'react-intl';
import classNames from 'classnames';
import {
Avatar,
@ -10,6 +10,7 @@ import {
MenuItem,
NamedLink,
} from '../../components';
import { TopbarSearchForm } from '../../containers';
import logo from './images/saunatime-logo.png';
import css from './TopbarDesktop.css';
@ -24,16 +25,20 @@ const TopbarDesktop = props => {
intl,
isAuthenticated,
onLogout,
onSearchSubmit,
initialSearchFormValues,
} = props;
const rootClass = rootClassName || css.root;
const classes = classNames(rootClass, className);
// TODO This is just a placeholder
const search = (
<div className={css.searchLink}>
<span className={css.search}>Search should be here</span>
</div>
<TopbarSearchForm
className={css.searchLink}
form="TopbarSearchFormDesktop"
onSubmit={onSearchSubmit}
initialValues={initialSearchFormValues}
/>
);
const inboxLink = isAuthenticated
@ -83,7 +88,6 @@ const TopbarDesktop = props => {
/>
</NamedLink>
{search}
<div className={css.spacer} />
<NamedLink className={css.createListingLink} name="NewListingPage">
<span className={css.createListing}>
<FormattedMessage id="TopbarDesktop.createListing" />
@ -97,9 +101,15 @@ const TopbarDesktop = props => {
);
};
TopbarDesktop.defaultProps = { className: null, firstName: '', lastName: '', rootClassName: '' };
const { bool, func, string, object } = PropTypes;
const { bool, func, string } = PropTypes;
TopbarDesktop.defaultProps = {
firstName: '',
lastName: '',
className: null,
rootClassName: null,
initialSearchFormValues: {},
};
TopbarDesktop.propTypes = {
className: string,
@ -109,9 +119,9 @@ TopbarDesktop.propTypes = {
firstName: string,
lastName: string,
rootClassName: string,
// from injectIntl
onSearchSubmit: func.isRequired,
initialSearchFormValues: object,
intl: intlShape.isRequired,
};
export default injectIntl(TopbarDesktop);
export default TopbarDesktop;

View file

@ -10,18 +10,22 @@ const noop = () => null;
describe('TopbarDesktop', () => {
it('data matches snapshot', () => {
window.google = { maps: {} };
const flattenedRoutes = flattenRoutes(routesConfiguration);
const topbarProps = {
isAuthenticated: true,
currentUserHasListings: true,
name: 'John Doe',
onSearchSubmit: noop,
intl: fakeIntl,
onLogout: noop,
};
const tree = renderDeep(
<RoutesProvider flattenedRoutes={flattenedRoutes}>
<TopbarDesktop
isAuthenticated
currentUserHasListings
name="John Doe"
intl={fakeIntl}
onLogout={noop}
/>
<TopbarDesktop {...topbarProps} />
</RoutesProvider>
);
delete window.google;
expect(tree).toMatchSnapshot();
});
});

View file

@ -7,19 +7,54 @@ exports[`TopbarDesktop data matches snapshot 1`] = `
onClick={[Function]}
style={Object {}}>
<img
alt="Logo"
alt="TopbarDesktop.logo"
className={undefined}
src="saunatime-logo.png" />
</a>
<div
className={undefined}>
<span
className={undefined}>
Search should be here
</span>
</div>
<div
className={undefined} />
<form
className=""
onSubmit={[Function]}>
<div
className="">
<div
className="">
<svg
className={undefined}
height="22"
viewBox="0 0 21 22"
width="21"
xmlns="http://www.w3.org/2000/svg">
<g
className={undefined}
fill="none"
fillRule="evenodd"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
transform="matrix(-1 0 0 1 20 1)">
<path
d="M13 14l5.241 5.241" />
<circle
cx="7.5"
cy="7.5"
r="7.5" />
</g>
</svg>
</div>
<input
autoComplete="off"
autoFocus={false}
className=""
name="location"
onBlur={[Function]}
onChange={[Function]}
onFocus={[Function]}
onKeyDown={[Function]}
placeholder="Search saunas..."
type="search"
value="" />
</div>
</form>
<a
className=""
href="/l/new"

View file

@ -15,7 +15,6 @@
font-family: inherit;
/* Border */
border: 1px solid #979797;
border-radius: 0px; /* Remove default iOS border radius */
/* Dimensions */

View file

@ -14,7 +14,7 @@ const SearchForm = props => {
return (
<form {...addClassName} onSubmit={handleSubmit}>
<Field
className={css.locationInput}
inputClassName={css.locationInput}
name="location"
label="Location"
placeholder={intl.formatMessage({ id: 'SearchForm.placeholder' })}

View file

@ -2,7 +2,32 @@ exports[`SearchForm matches snapshot 1`] = `
<form
onSubmit={[Function]}>
<div
className={undefined}>
className="">
<div
className="">
<svg
className={undefined}
height="22"
viewBox="0 0 21 22"
width="21"
xmlns="http://www.w3.org/2000/svg">
<g
className={undefined}
fill="none"
fillRule="evenodd"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
transform="matrix(-1 0 0 1 20 1)">
<path
d="M13 14l5.241 5.241" />
<circle
cx="7.5"
cy="7.5"
r="7.5" />
</g>
</svg>
</div>
<input
autoComplete="off"
autoFocus={false}

View file

@ -4,7 +4,7 @@
display: flex;
flex-direction: column;
@media (min-width: 768px) {
@media (--desktopViewport) {
flex-direction: row;
}
}
@ -12,7 +12,7 @@
.withPadding {
padding: calc(2 * var(--spacingUnit));
@media (min-width: 768px) {
@media (--desktopViewport) {
padding: calc(2 * var(--spacingUnitDesktop));
}
}
@ -21,7 +21,7 @@
.defaultWrapperStyles {
margin: calc(2 * var(--spacingUnit)) 0;
@media (min-width: 768px) {
@media (--desktopViewport) {
margin: calc(2 * var(--spacingUnitDesktop)) 0;
}
}
@ -37,7 +37,7 @@
flex-grow: 1;
composes: withPadding;
@media (min-width: 768px) {
@media (--desktopViewport) {
padding: 56px 0 0 16px;
}
}
@ -66,7 +66,7 @@
background-image: url('data:image/svg+xml;utf8,<svg width="10" height="24" viewBox="0 0 10 24" xmlns="http://www.w3.org/2000/svg"><path d="M0 5h10M0 11h10M0 17h10" stroke="#f1f1f1"/><path d="M0 23h10" stroke="#ddd" stroke-width="1"/></svg>');
background-repeat: repeat;
@media (min-width: 768px) {
@media (--desktopViewport) {
background-image: url('data:image/svg+xml;utf8,<svg width="10" height="24" viewBox="0 0 10 24" xmlns="http://www.w3.org/2000/svg"><path d="M0 7h10M0 15h10M0" stroke="#f1f1f1"/><path d="M0 23h10" stroke="#ddd" stroke-width="1"/></svg>');
background-repeat: repeat;
}

View file

@ -23,7 +23,7 @@
/* shadows */
box-shadow: 0 1px 1px 0 #dcdcdc;
@media (min-width: 768px) {
@media (--desktopViewport) {
display: none;
}
}
@ -44,14 +44,10 @@
width: 44px;
}
.searchForm {
margin: 2rem;
}
.desktop {
display: none;
@media (min-width: 768px) {
@media (--desktopViewport) {
display: block;
}
}
@ -61,3 +57,21 @@
.rootSearchIcon {
stroke: var(--matterColor);
}
.searchContainer {
position: relative;
height: 100%;
}
.mobileHelp {
color: var(--matterColorAnti);
margin: 0 24px;
/* Absolute position to avoid affecting the layout of the autocomplete
predictions list */
position: absolute;
top: 115px;
/* Pull text behind search suggestions */
z-index: -1;
}

View file

@ -1,11 +1,11 @@
import React, { Component, PropTypes } from 'react';
import { compose } from 'redux';
import { connect } from 'react-redux';
import { intlShape, injectIntl } from 'react-intl';
import { FormattedMessage, intlShape, injectIntl } from 'react-intl';
import { pickBy } from 'lodash';
import { logout, authenticationInProgress } from '../../ducks/Auth.duck';
import { FlatButton, Modal, NamedLink, TopbarDesktop, TopbarMobileMenu } from '../../components';
import { SearchForm } from '../../containers';
import { TopbarSearchForm } from '../../containers';
import { withFlattenedRoutes } from '../../util/contextHelpers';
import { parse, stringify } from '../../util/urlHelpers';
import { ensureUser } from '../../util/data';
@ -63,10 +63,10 @@ class TopbarComponent extends Component {
}
handleSubmit(values) {
const selectedPlace = values && values.location ? values.location.selectedPlace : null;
const { search, selectedPlace } = values.location;
const { flattenedRoutes, history } = this.props;
const { address, origin, bounds, country } = selectedPlace || {};
const searchParams = { address, origin, bounds, country };
const { origin, bounds, country } = selectedPlace;
const searchParams = { address: search, origin, bounds, country };
history.push(createResourceLocatorString('SearchPage', flattenedRoutes, {}, searchParams));
}
@ -142,6 +142,8 @@ class TopbarComponent extends Component {
onLogout={this.handleLogout}
firstName={profile.firstName}
lastName={profile.lastName}
onSearchSubmit={this.handleSubmit}
initialSearchFormValues={initialSearchFormValues}
/>
</div>
<Modal
@ -156,12 +158,17 @@ class TopbarComponent extends Component {
onClose={this.handleMobileSearchClose}
togglePageClassNames={togglePageClassNames}
>
<SearchForm
form="TopbarSearchForm"
className={css.searchForm}
onSubmit={this.handleSubmit}
initialValues={initialSearchFormValues}
/>
<div className={css.searchContainer}>
<TopbarSearchForm
form="TopbarSearchForm"
onSubmit={this.handleSubmit}
initialValues={initialSearchFormValues}
isMobile
/>
<p className={css.mobileHelp}>
<FormattedMessage id="Topbar.mobileSearchHelp" />
</p>
</div>
</Modal>
</div>
);

View file

@ -0,0 +1,39 @@
@import '../../marketplace.css';
:root {
--inputHeight: 50px;
--topbarHeight: 72px;
}
.mobileInputRoot {
margin-left: 24px;
margin-right: 24px;
}
.desktopInputRoot {
height: var(--topbarHeight);
}
.desktopIcon {
border: none;
}
.desktopInput {
height: var(--topbarHeight);
border: none;
padding-top: 0;
padding-bottom: 0;
}
.mobilePredictions {
position: fixed;
top: 113px;
left: 0;
right: 0;
bottom: 0;
}
.desktopPredictions {
margin-top: calc(var(--topbarHeight) - var(--inputHeight));
max-width: 434px;
}

View file

@ -0,0 +1,68 @@
import React, { PropTypes } from 'react';
import { compose } from 'redux';
import { Field, reduxForm, propTypes as formPropTypes } from 'redux-form';
import { intlShape, injectIntl } from 'react-intl';
import classNames from 'classnames';
import { LocationAutocompleteInput } from '../../components';
import css from './TopbarSearchForm.css';
const TopbarSearchFormComponent = props => {
const { rootClassName, className, intl, onSubmit, isMobile } = props;
const onChange = location => {
if (location.selectedPlace) {
// Note that we use `onSubmit` instead of the conventional
// `handleSubmit` prop for submitting. We want to autosubmit
// when a place is selected, and don't require any extra
// validations for the form.
onSubmit({ location });
}
};
const classes = classNames(rootClassName, className);
// Allow form submit only when the place has changed
const preventFormSubmit = e => e.preventDefault();
return (
<form className={classes} onSubmit={preventFormSubmit}>
<Field
name="location"
label="Location"
className={isMobile ? css.mobileInputRoot : css.desktopInputRoot}
iconClassName={isMobile ? null : css.desktopIcon}
inputClassName={isMobile ? null : css.desktopInput}
predictionsClassName={isMobile ? css.mobilePredictions : css.desktopPredictions}
placeholder={intl.formatMessage({ id: 'TopbarSearchForm.placeholder' })}
format={null}
component={LocationAutocompleteInput}
onChange={onChange}
/>
</form>
);
};
const { func, string, bool } = PropTypes;
TopbarSearchFormComponent.defaultProps = { rootClassName: null, className: null, isMobile: false };
TopbarSearchFormComponent.propTypes = {
...formPropTypes,
rootClassName: string,
className: string,
onSubmit: func.isRequired,
isMobile: bool,
// from injectIntl
intl: intlShape.isRequired,
};
const defaultFormName = 'TopbarSearchForm';
const TopbarSearchForm = compose(reduxForm({ form: defaultFormName }), injectIntl)(
TopbarSearchFormComponent
);
export default TopbarSearchForm;

View file

@ -31,6 +31,7 @@ import SignupForm from './SignupForm/SignupForm';
import StripePaymentForm from './StripePaymentForm/StripePaymentForm';
import StyleguidePage from './StyleguidePage/StyleguidePage';
import Topbar from './Topbar/Topbar';
import TopbarSearchForm from './TopbarSearchForm/TopbarSearchForm';
export {
AuthenticationPage,
@ -66,4 +67,5 @@ export {
StripePaymentForm,
StyleguidePage,
Topbar,
TopbarSearchForm,
};

View file

@ -1,5 +1,7 @@
@import "sanitize.css";
/* ================ Custom properties aka variables ================ */
:root {
/* Colors */
--marketplaceColor: #C0392B;
@ -37,7 +39,12 @@
--borderRadius: 2px;
}
/* FONTS */
/* ================ Custom media queries ================ */
@custom-media --desktopViewport (min-width: 768px);
/* ================ Fonts ================ */
h1, h2, h3, h4, h5, h6, p, pre {
margin: 0;
}
@ -52,7 +59,7 @@ h1,
letter-spacing: -1px;
/* TODO: this seems to be biggest font in Page titles (except in Hero) - is it h1 then? */
@media (min-width: 768px) {
@media (--desktopViewport) {
font-family: "sofiapro";
font-weight: 600;
font-size: 48px;
@ -72,7 +79,7 @@ h2,
letter-spacing: -0.5px;
/* TODO Are these correct settings? */
@media (min-width: 768px) {
@media (--desktopViewport) {
font-size: 24px;
line-height: 32px;
padding: 4px 0 4px 0; /* 4px + 4px = 8px */
@ -133,6 +140,7 @@ input,
button,
p,
pre,
li,
.bodyFont {
font-family: "sofiapro", Helvetica, Arial, sans-serif;
font-weight: 500;
@ -142,7 +150,7 @@ pre,
letter-spacing: -0.1px;
/* TODO Desktop styles needs to be extracted */
@media (min-width: 768px) {
@media (--desktopViewport) {
font-weight: 500;
font-size: 16px;
line-height: 24px;
@ -191,13 +199,17 @@ pre,
letter-spacing: 0.1px;
}
.searchResultsFont {
composes: bodyFont;
}
/* Base font color */
html,
.textColor {
color: var(--matterColor);
}
/* NORMALIZATIONS for other elements */
/* ================ Normalisations ================ */
/* TODO */
html {
@ -231,5 +243,25 @@ select {
textarea {
width: 100%;
border: 1px solid #ddd;
border-width: 1px;
border-style: solid;
border-color: var(--marketplaceColor);
}
input {
display: block;
width: 100%;
border: none;
border-bottom-width: 3px;
border-bottom-style: solid;
border-bottom-color: var(--marketplaceColor);
&::placeholder {
color: var(--matterColorAnti);
}
&:focus {
outline: none;
}
}

View file

@ -39,19 +39,19 @@
"EditListingDescriptionForm.titleRequired": "You need to add a name.",
"EditListingDescriptionPanel.title": "Add your sauna",
"EditListingLocationForm.address": "Address",
"EditListingLocationForm.addressRequired": "You need to provide a location",
"EditListingLocationForm.addressNotRecognized": "We didn't recognize this location. Please try another location.",
"EditListingLocationForm.addressRequired": "You need to provide a location",
"EditListingLocationForm.building": "Apt, suite, building # (optional)",
"EditListingLocationForm.buildingPlaceholder": "E.g. A 42",
"EditListingLocationPanel.title": "Where's your sauna located?",
"EditListingPhotosForm.imageRequired": "You need to add at least one image.",
"EditListingPage.titleCreateListing": "Create a listing",
"EditListingPage.titleEditListing": "Edit listing",
"EditListingPhotosForm.bankAccountNumberRequired": "You need to add a bank account number.",
"EditListingPhotosForm.imageRequired": "You need to add at least one image.",
"EditListingPhotosPanel.title": "Add photos",
"EditListingPricingForm.perNight": "per night",
"EditListingPricingForm.priceRequired": "You need to add a valid price.",
"EditListingPricingPanel.title": "How much does it cost?",
"EditListingPage.titleCreateListing": "Create a listing",
"EditListingPage.titleEditListing": "Edit listing",
"HeroSection.subTitle": "The largest online community to rent music studios",
"HeroSection.title": "Book Studiotime anywhere",
"InboxPage.fetchFailed": "Could not load all messages. Please try again.",
@ -154,11 +154,11 @@
"SignupForm.termsOfService": "By confirming I accept the terms and conditions and the {stripeConnectedAccountAgreementLink}",
"StripeBankAccountToken.accountNumberPlaceholder": " ",
"StripeBankAccountToken.bankAccountNumberLabel": "Bank account number:",
"StripeBankAccountToken.routingNumberLabel": "Routing number:",
"StripeBankAccountToken.bankAccountNumberLabelIban": "Bank account number (IBAN):",
"StripeBankAccountToken.createBankAccountTokenError": "Could not connect account number. Please double-check that your routing number and account number are valid in {country} when using {currency}",
"StripeBankAccountToken.createBankAccountTokenErrorIban": "Could not connect account number. Please double-check that your account number is valid in {country} when using {currency}",
"StripeBankAccountToken.invalidRoutingNumber": "Invalid routing number {number} for country {country}",
"StripeBankAccountToken.routingNumberLabel": "Routing number:",
"StripeBankAccountToken.routingNumberPlaceholder": " ",
"StripeBankAccountToken.unsupportedCountry": "Country not supported: {country}",
"StripePaymentForm.genericError": "Could not handle payment data. Please try again.",
@ -185,12 +185,13 @@
"StripePaymentForm.submitPaymentInfo": "Send request",
"Topbar.logoIcon": "Go to homepage",
"Topbar.menuIcon": "Open menu",
"Topbar.mobileSearchHelp": "Tip: You can also search saunas by zip code, for example \"00500\" or city district \"Sörnäinen\".",
"Topbar.searchIcon": "Open search",
"TopbarDesktop.createListing": "+ Add your sauna",
"TopbarDesktop.inbox": "Inbox",
"TopbarDesktop.login": "Log in",
"TopbarDesktop.logout": "Log out",
"TopbarDesktop.logo": "Logo",
"TopbarDesktop.logout": "Log out",
"TopbarDesktop.signup": "Sign up",
"TopbarMobileMenu.greeting": "Hello {firstName}",
"TopbarMobileMenu.inboxLink": "Inbox",
@ -199,5 +200,8 @@
"TopbarMobileMenu.newListingLink": "+ Add your sauna",
"TopbarMobileMenu.signupLink": "Sign up",
"TopbarMobileMenu.signupOrLogin": "{signup} or {login}",
"TopbarMobileMenu.unauthorizedGreeting": "Hello there,{lineBreak}would you like to {signupOrLogin}?"
"TopbarMobileMenu.unauthorizedGreeting": "Hello there,{lineBreak}would you like to {signupOrLogin}?",
"TopbarSearchForm.placeholder": "Search saunas...",
"TopbarSearchForm.placeholder": "Search saunas...",
"TopbarSearchForm.searchHelp": "Tip: You can also search saunas by zip code, for example \"00500\" or city district \"Sörnäinen\"."
}