mirror of
https://github.com/kingomarnajjar/flex-template-web.git
synced 2026-07-26 06:47:17 +10:00
Merge pull request #27 from sharetribe/searchpage-wires
Searchpage wires
This commit is contained in:
commit
dbda88f364
31 changed files with 1295 additions and 30 deletions
|
|
@ -4,6 +4,7 @@
|
|||
"private": true,
|
||||
"dependencies": {
|
||||
"basic-auth": "^1.1.0",
|
||||
"classnames": "^2.2.5",
|
||||
"compression": "^1.6.2",
|
||||
"express": "^4.14.0",
|
||||
"helmet": "^3.3.0",
|
||||
|
|
|
|||
37
src/components/FilterPanel/FilterPanel.css
Normal file
37
src/components/FilterPanel/FilterPanel.css
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
.filterTitle {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.toListingsButton {
|
||||
position: fixed;
|
||||
bottom: 1rem;
|
||||
left: 50%;
|
||||
width: 200px;
|
||||
margin-left: -100px;
|
||||
display: block;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
font-size: 1.4rem;
|
||||
padding: 0.5rem;
|
||||
color: #fff;
|
||||
background-color: #9B9B9B;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: #888;
|
||||
}
|
||||
}
|
||||
|
||||
.close {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 3em;
|
||||
padding: 1em;
|
||||
color: #999;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
color: #000;
|
||||
}
|
||||
}
|
||||
13
src/components/FilterPanel/FilterPanel.js
Normal file
13
src/components/FilterPanel/FilterPanel.js
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import React from 'react';
|
||||
import { NamedLink } from '../../components';
|
||||
import css from './FilterPanel.css';
|
||||
|
||||
const FilterPanel = () => (
|
||||
<div>
|
||||
<h1 className={css.filterTitle}>Filters</h1>
|
||||
<NamedLink className={css.toListingsButton} name="SearchListingsPage">See studios</NamedLink>
|
||||
<NamedLink className={css.close} name="SearchListingsPage">X</NamedLink>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default FilterPanel;
|
||||
22
src/components/FilterPanel/FilterPanel.test.js
Normal file
22
src/components/FilterPanel/FilterPanel.test.js
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import React from 'react';
|
||||
import renderer from 'react-test-renderer';
|
||||
import { TestProvider } from '../../util/test-helpers';
|
||||
import { RoutesProvider } from '../../components';
|
||||
import routesConfiguration from '../../routesConfiguration';
|
||||
import FilterPanel from './FilterPanel';
|
||||
|
||||
describe('FilterPanel', () => {
|
||||
it('matches snapshot', () => {
|
||||
const component = renderer.create(
|
||||
(
|
||||
<TestProvider>
|
||||
<RoutesProvider routes={routesConfiguration}>
|
||||
<FilterPanel />
|
||||
</RoutesProvider>
|
||||
</TestProvider>
|
||||
),
|
||||
);
|
||||
const tree = component.toJSON();
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
exports[`FilterPanel matches snapshot 1`] = `
|
||||
<div>
|
||||
<h1
|
||||
className={undefined}>
|
||||
Filters
|
||||
</h1>
|
||||
<a
|
||||
className=""
|
||||
href="/s/listings"
|
||||
onClick={[Function]}
|
||||
style={Object {}}>
|
||||
See studios
|
||||
</a>
|
||||
<a
|
||||
className=""
|
||||
href="/s/listings"
|
||||
onClick={[Function]}
|
||||
style={Object {}}>
|
||||
X
|
||||
</a>
|
||||
</div>
|
||||
`;
|
||||
67
src/components/ListingCard/ListingCard.css
Normal file
67
src/components/ListingCard/ListingCard.css
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
.listing {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.squareWrapper {
|
||||
display: block;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Firefox doesn't support image aspect ratio inside flexbox */
|
||||
.aspectWrapper {
|
||||
padding-bottom: 75%; /* 4:3 Aspect Ratio */
|
||||
}
|
||||
|
||||
.thumbnail {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.info {
|
||||
padding: 1rem;
|
||||
|
||||
}
|
||||
|
||||
.mainInfo {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.details {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.authorInfo {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.authorDetails {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.avatarWrapper {
|
||||
display: block;
|
||||
flex-basis: 44px;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
.avatar {
|
||||
border-radius: 22px;
|
||||
}
|
||||
71
src/components/ListingCard/ListingCard.js
Normal file
71
src/components/ListingCard/ListingCard.js
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
import React, { PropTypes } from 'react';
|
||||
import { NamedLink } from '../../components';
|
||||
import css from './ListingCard.css';
|
||||
|
||||
const ListingCard = props => {
|
||||
const { id, title, price, description, location, review, author } = props;
|
||||
const slug = encodeURIComponent(title.split(' ').join('-'));
|
||||
return (
|
||||
<div className={css.listing}>
|
||||
<div className={css.squareWrapper}>
|
||||
<div className={css.aspectWrapper}>
|
||||
<img className={css.thumbnail} src="http://placehold.it/400x300" alt="Listing Title" />
|
||||
</div>
|
||||
</div>
|
||||
<div className={css.info}>
|
||||
<div className={css.mainInfo}>
|
||||
<NamedLink className={css.title} name="ListingPage" params={{ id, slug }}>
|
||||
{title}
|
||||
</NamedLink>
|
||||
<div className={css.price}>
|
||||
{price}
|
||||
</div>
|
||||
</div>
|
||||
<div className={css.description}>
|
||||
{description}
|
||||
</div>
|
||||
<div className={css.details}>
|
||||
<div className={css.location}>
|
||||
{location}
|
||||
</div>
|
||||
<div className={css.reviews}>
|
||||
(<span>{review.rating}</span><span>/5</span>){' '}
|
||||
<span>{review.count}</span>
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<div className={css.authorInfo}>
|
||||
<div className={css.avatarWrapper}>
|
||||
<img className={css.avatar} src={author.avatar} alt={author.name} />
|
||||
</div>
|
||||
<div className={css.authorDetails}>
|
||||
<span className={css.authorName}>{author.name}</span>
|
||||
<div className={css.authorReview}>
|
||||
review: <span>{author.review.rating}</span><span>/5</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ListingCard.defaultProps = { location: null, review: {}, author: {} };
|
||||
|
||||
const { number, shape, string } = PropTypes;
|
||||
|
||||
ListingCard.propTypes = {
|
||||
id: number.isRequired,
|
||||
title: string.isRequired,
|
||||
price: string.isRequired,
|
||||
description: string.isRequired,
|
||||
location: string,
|
||||
review: shape({ rating: string.isRequired, count: string.isRequired }),
|
||||
author: shape({
|
||||
name: string.isRequired,
|
||||
avatar: string.isRequired,
|
||||
review: shape({ rating: string.isRequired }),
|
||||
}),
|
||||
};
|
||||
|
||||
export default ListingCard;
|
||||
35
src/components/ListingCard/ListingCard.test.js
Normal file
35
src/components/ListingCard/ListingCard.test.js
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import React from 'react';
|
||||
import renderer from 'react-test-renderer';
|
||||
import { TestProvider } from '../../util/test-helpers';
|
||||
import { RoutesProvider } from '../../components';
|
||||
import routesConfiguration from '../../routesConfiguration';
|
||||
import ListingCard from './ListingCard';
|
||||
|
||||
describe('ListingCard', () => {
|
||||
it('matches snapshot', () => {
|
||||
const listing = {
|
||||
id: 123,
|
||||
title: 'Banyan Studios',
|
||||
price: '55\u20AC / day',
|
||||
description: 'Organic Music Production in a Sustainable, Ethical and Professional Studio.',
|
||||
location: 'New York, NY \u2022 40mi away',
|
||||
review: { count: '8 reviews', rating: '4' },
|
||||
author: {
|
||||
name: 'The Stardust Collective',
|
||||
avatar: 'http://placehold.it/44x44',
|
||||
review: { rating: '4' },
|
||||
},
|
||||
};
|
||||
const component = renderer.create(
|
||||
(
|
||||
<TestProvider>
|
||||
<RoutesProvider routes={routesConfiguration}>
|
||||
<ListingCard {...listing} />
|
||||
</RoutesProvider>
|
||||
</TestProvider>
|
||||
),
|
||||
);
|
||||
const tree = component.toJSON();
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
exports[`ListingCard matches snapshot 1`] = `
|
||||
<div
|
||||
className={undefined}>
|
||||
<div
|
||||
className={undefined}>
|
||||
<div
|
||||
className={undefined}>
|
||||
<img
|
||||
alt="Listing Title"
|
||||
className={undefined}
|
||||
src="http://placehold.it/400x300" />
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={undefined}>
|
||||
<div
|
||||
className={undefined}>
|
||||
<a
|
||||
className=""
|
||||
href="/l/Banyan-Studios/123"
|
||||
onClick={[Function]}
|
||||
style={Object {}}>
|
||||
Banyan Studios
|
||||
</a>
|
||||
<div
|
||||
className={undefined}>
|
||||
55€ / day
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={undefined}>
|
||||
Organic Music Production in a Sustainable, Ethical and Professional Studio.
|
||||
</div>
|
||||
<div
|
||||
className={undefined}>
|
||||
<div
|
||||
className={undefined}>
|
||||
New York, NY • 40mi away
|
||||
</div>
|
||||
<div
|
||||
className={undefined}>
|
||||
(
|
||||
<span>
|
||||
4
|
||||
</span>
|
||||
<span>
|
||||
/5
|
||||
</span>
|
||||
)
|
||||
|
||||
<span>
|
||||
8 reviews
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<div
|
||||
className={undefined}>
|
||||
<div
|
||||
className={undefined}>
|
||||
<img
|
||||
alt="The Stardust Collective"
|
||||
className={undefined}
|
||||
src="http://placehold.it/44x44" />
|
||||
</div>
|
||||
<div
|
||||
className={undefined}>
|
||||
<span
|
||||
className={undefined}>
|
||||
The Stardust Collective
|
||||
</span>
|
||||
<div
|
||||
className={undefined}>
|
||||
review:
|
||||
<span>
|
||||
4
|
||||
</span>
|
||||
<span>
|
||||
/5
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
38
src/components/ListingCardSmall/ListingCardSmall.css
Normal file
38
src/components/ListingCardSmall/ListingCardSmall.css
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
.listing {
|
||||
width: 130px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: 4px;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
.squareWrapper {
|
||||
display: block;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Firefox doesn't support image aspect ratio inside flexbox */
|
||||
.aspectWrapper {
|
||||
padding-bottom: 75%; /* 4:3 Aspect Ratio */
|
||||
}
|
||||
|
||||
.thumbnail {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.info {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
padding: 0.5rem;
|
||||
font-size: 0.8rem;
|
||||
border: solid 1px #ccc;
|
||||
}
|
||||
43
src/components/ListingCardSmall/ListingCardSmall.js
Normal file
43
src/components/ListingCardSmall/ListingCardSmall.js
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import React, { PropTypes } from 'react';
|
||||
import { NamedLink } from '../../components';
|
||||
import css from './ListingCardSmall.css';
|
||||
|
||||
// <NamedLink name="SearchListingsPage">X</NamedLink>
|
||||
const ListingCardSmall = props => {
|
||||
const { id, title, price, review } = props;
|
||||
const slug = encodeURIComponent(title.split(' ').join('-'));
|
||||
return (
|
||||
<div className={css.listing}>
|
||||
<div className={css.squareWrapper}>
|
||||
<div className={css.aspectWrapper}>
|
||||
<img className={css.thumbnail} src="http://placehold.it/200x150" alt="Listing Title" />
|
||||
</div>
|
||||
</div>
|
||||
<div className={css.info}>
|
||||
<NamedLink className={css.title} name="ListingPage" params={{ id, slug }}>
|
||||
{title}
|
||||
</NamedLink>
|
||||
<div className={css.reviews}>
|
||||
(<span>{review.rating}</span><span>/5</span>){' '}
|
||||
<span>{review.count}</span>
|
||||
</div>
|
||||
<div className={css.price}>
|
||||
{price}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ListingCardSmall.defaultProps = { review: {} };
|
||||
|
||||
const { number, shape, string } = PropTypes;
|
||||
|
||||
ListingCardSmall.propTypes = {
|
||||
id: number.isRequired,
|
||||
title: string.isRequired,
|
||||
price: string.isRequired,
|
||||
review: shape({ rating: string.isRequired, count: string.isRequired }),
|
||||
};
|
||||
|
||||
export default ListingCardSmall;
|
||||
35
src/components/ListingCardSmall/ListingCardSmall.test.js
Normal file
35
src/components/ListingCardSmall/ListingCardSmall.test.js
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import React from 'react';
|
||||
import renderer from 'react-test-renderer';
|
||||
import { TestProvider } from '../../util/test-helpers';
|
||||
import { RoutesProvider } from '../../components';
|
||||
import routesConfiguration from '../../routesConfiguration';
|
||||
import ListingCardSmall from './ListingCardSmall';
|
||||
|
||||
describe('ListingCardSmall', () => {
|
||||
it('matches snapshot', () => {
|
||||
const listing = {
|
||||
id: 123,
|
||||
title: 'Banyan Studios',
|
||||
price: '55\u20AC / day',
|
||||
description: 'Organic Music Production in a Sustainable, Ethical and Professional Studio.',
|
||||
location: 'New York, NY \u2022 40mi away',
|
||||
review: { count: '8 reviews', rating: '4' },
|
||||
author: {
|
||||
name: 'The Stardust Collective',
|
||||
avatar: 'http://placehold.it/44x44',
|
||||
review: { rating: '4' },
|
||||
},
|
||||
};
|
||||
const component = renderer.create(
|
||||
(
|
||||
<TestProvider>
|
||||
<RoutesProvider routes={routesConfiguration}>
|
||||
<ListingCardSmall {...listing} />
|
||||
</RoutesProvider>
|
||||
</TestProvider>
|
||||
),
|
||||
);
|
||||
const tree = component.toJSON();
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
exports[`ListingCardSmall matches snapshot 1`] = `
|
||||
<div
|
||||
className={undefined}>
|
||||
<div
|
||||
className={undefined}>
|
||||
<div
|
||||
className={undefined}>
|
||||
<img
|
||||
alt="Listing Title"
|
||||
className={undefined}
|
||||
src="http://placehold.it/200x150" />
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={undefined}>
|
||||
<a
|
||||
className=""
|
||||
href="/l/Banyan-Studios/123"
|
||||
onClick={[Function]}
|
||||
style={Object {}}>
|
||||
Banyan Studios
|
||||
</a>
|
||||
<div
|
||||
className={undefined}>
|
||||
(
|
||||
<span>
|
||||
4
|
||||
</span>
|
||||
<span>
|
||||
/5
|
||||
</span>
|
||||
)
|
||||
|
||||
<span>
|
||||
8 reviews
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className={undefined}>
|
||||
55€ / day
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
53
src/components/MapPanel/MapPanel.css
Normal file
53
src/components/MapPanel/MapPanel.css
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
.mapContainer {
|
||||
height: 70vh;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #ddd;
|
||||
color: #fff;
|
||||
font-size: 2rem;
|
||||
}
|
||||
.mapListingsContainer {
|
||||
height: 30vh;
|
||||
width: 100%;
|
||||
background-color: #fff;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
|
||||
.toFiltersButton {
|
||||
position: fixed;
|
||||
top: calc(70vh - 80px);
|
||||
left: 50%;
|
||||
width: 200px;
|
||||
margin-left: -100px;
|
||||
display: block;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
font-size: 1.4rem;
|
||||
padding: 0.5rem;
|
||||
color: #fff;
|
||||
background-color: #9B9B9B;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: #888;
|
||||
}
|
||||
}
|
||||
|
||||
.close {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 3em;
|
||||
padding: 1em;
|
||||
color: #999;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
color: #000;
|
||||
}
|
||||
}
|
||||
22
src/components/MapPanel/MapPanel.js
Normal file
22
src/components/MapPanel/MapPanel.js
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import React, { PropTypes } from 'react';
|
||||
import { NamedLink } from '../../components';
|
||||
import css from './MapPanel.css';
|
||||
|
||||
const MapPanel = props => (
|
||||
<div>
|
||||
<div className={css.mapContainer}>Map</div>
|
||||
<div className={css.mapListingsContainer}>
|
||||
{props.children}
|
||||
</div>
|
||||
<NamedLink className={css.toFiltersButton} name="SearchFiltersPage">Filters</NamedLink>
|
||||
<NamedLink className={css.close} name="SearchListingsPage">X</NamedLink>
|
||||
</div>
|
||||
);
|
||||
|
||||
MapPanel.defaultProps = { children: null };
|
||||
|
||||
const { any } = PropTypes;
|
||||
|
||||
MapPanel.propTypes = { children: any };
|
||||
|
||||
export default MapPanel;
|
||||
22
src/components/MapPanel/MapPanel.test.js
Normal file
22
src/components/MapPanel/MapPanel.test.js
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import React from 'react';
|
||||
import renderer from 'react-test-renderer';
|
||||
import { TestProvider } from '../../util/test-helpers';
|
||||
import { RoutesProvider } from '../../components';
|
||||
import routesConfiguration from '../../routesConfiguration';
|
||||
import MapPanel from './MapPanel';
|
||||
|
||||
describe('MapPanel', () => {
|
||||
it('matches snapshot', () => {
|
||||
const component = renderer.create(
|
||||
(
|
||||
<TestProvider>
|
||||
<RoutesProvider routes={routesConfiguration}>
|
||||
<MapPanel />
|
||||
</RoutesProvider>
|
||||
</TestProvider>
|
||||
),
|
||||
);
|
||||
const tree = component.toJSON();
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
24
src/components/MapPanel/__snapshots__/MapPanel.test.js.snap
Normal file
24
src/components/MapPanel/__snapshots__/MapPanel.test.js.snap
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
exports[`MapPanel matches snapshot 1`] = `
|
||||
<div>
|
||||
<div
|
||||
className={undefined}>
|
||||
Map
|
||||
</div>
|
||||
<div
|
||||
className={undefined} />
|
||||
<a
|
||||
className=""
|
||||
href="/s/filters"
|
||||
onClick={[Function]}
|
||||
style={Object {}}>
|
||||
Filters
|
||||
</a>
|
||||
<a
|
||||
className=""
|
||||
href="/s/listings"
|
||||
onClick={[Function]}
|
||||
style={Object {}}>
|
||||
X
|
||||
</a>
|
||||
</div>
|
||||
`;
|
||||
17
src/components/Menu/Menu.css
Normal file
17
src/components/Menu/Menu.css
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
.container {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 50px;
|
||||
border-top: solid 1px #f7f7f7;
|
||||
}
|
||||
.container:hover {
|
||||
background-color: #f7f7f7;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.openIndicator {
|
||||
margin-left: auto;
|
||||
margin-right: 1em;
|
||||
}
|
||||
10
src/components/Menu/Menu.js
Normal file
10
src/components/Menu/Menu.js
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import React from 'react';
|
||||
import css from './Menu.css';
|
||||
|
||||
const Menu = () => (
|
||||
<div className={css.container}>
|
||||
New York, Jan 2nd – Jan 4th <span className={css.openIndicator}>▼</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default Menu;
|
||||
14
src/components/Menu/Menu.test.js
Normal file
14
src/components/Menu/Menu.test.js
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import React from 'react';
|
||||
import renderer from 'react-test-renderer';
|
||||
import { TestProvider } from '../../util/test-helpers';
|
||||
import { RoutesProvider } from '../../components';
|
||||
import routesConfiguration from '../../routesConfiguration';
|
||||
import Menu from './Menu';
|
||||
|
||||
describe('Menu', () => {
|
||||
it('matches snapshot', () => {
|
||||
const component = renderer.create(<Menu />);
|
||||
const tree = component.toJSON();
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
10
src/components/Menu/__snapshots__/Menu.test.js.snap
Normal file
10
src/components/Menu/__snapshots__/Menu.test.js.snap
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
exports[`Menu matches snapshot 1`] = `
|
||||
<div
|
||||
className={undefined}>
|
||||
New York, Jan 2nd – Jan 4th
|
||||
<span
|
||||
className={undefined}>
|
||||
▼
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
24
src/components/SearchResultsPanel/SearchResultsPanel.css
Normal file
24
src/components/SearchResultsPanel/SearchResultsPanel.css
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
.navigation {
|
||||
display: flex;
|
||||
position: fixed;
|
||||
bottom: 15px;
|
||||
left: 50%;
|
||||
margin-left: -100px;
|
||||
}
|
||||
|
||||
.button {
|
||||
display: block;
|
||||
width: 100px;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
font-size: 1.4rem;
|
||||
padding: 0.5rem;
|
||||
margin: 1rem 0.1rem;
|
||||
color: #fff;
|
||||
background-color: #9B9B9B;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: #888;
|
||||
}
|
||||
}
|
||||
22
src/components/SearchResultsPanel/SearchResultsPanel.js
Normal file
22
src/components/SearchResultsPanel/SearchResultsPanel.js
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import React, { PropTypes } from 'react';
|
||||
import { Menu, NamedLink } from '../../components';
|
||||
import css from './SearchResultsPanel.css';
|
||||
|
||||
const SearchResultsPanel = props => (
|
||||
<div>
|
||||
<Menu />
|
||||
{props.children}
|
||||
<div className={css.navigation}>
|
||||
<NamedLink className={css.button} name="SearchFiltersPage">Filters</NamedLink>
|
||||
<NamedLink className={css.button} name="SearchMapPage">Map</NamedLink>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
SearchResultsPanel.defaultProps = { children: null };
|
||||
|
||||
const { any } = PropTypes;
|
||||
|
||||
SearchResultsPanel.propTypes = { children: any };
|
||||
|
||||
export default SearchResultsPanel;
|
||||
22
src/components/SearchResultsPanel/SearchResultsPanel.test.js
Normal file
22
src/components/SearchResultsPanel/SearchResultsPanel.test.js
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import React from 'react';
|
||||
import renderer from 'react-test-renderer';
|
||||
import { TestProvider } from '../../util/test-helpers';
|
||||
import { RoutesProvider } from '../../components';
|
||||
import routesConfiguration from '../../routesConfiguration';
|
||||
import SearchResultsPanel from './SearchResultsPanel';
|
||||
|
||||
describe('SearchResultsPanel', () => {
|
||||
it('matches snapshot', () => {
|
||||
const component = renderer.create(
|
||||
(
|
||||
<TestProvider>
|
||||
<RoutesProvider routes={routesConfiguration}>
|
||||
<SearchResultsPanel />
|
||||
</RoutesProvider>
|
||||
</TestProvider>
|
||||
),
|
||||
);
|
||||
const tree = component.toJSON();
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
exports[`SearchResultsPanel matches snapshot 1`] = `
|
||||
<div>
|
||||
<div
|
||||
className={undefined}>
|
||||
New York, Jan 2nd – Jan 4th
|
||||
<span
|
||||
className={undefined}>
|
||||
▼
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className={undefined}>
|
||||
<a
|
||||
className=""
|
||||
href="/s/filters"
|
||||
onClick={[Function]}
|
||||
style={Object {}}>
|
||||
Filters
|
||||
</a>
|
||||
<a
|
||||
className=""
|
||||
href="/s/map"
|
||||
onClick={[Function]}
|
||||
style={Object {}}>
|
||||
Map
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
|
@ -1,8 +1,27 @@
|
|||
import FilterPanel from './FilterPanel/FilterPanel';
|
||||
import HeroSection from './HeroSection/HeroSection';
|
||||
import ListingCard from './ListingCard/ListingCard';
|
||||
import ListingCardSmall from './ListingCardSmall/ListingCardSmall';
|
||||
import MapPanel from './MapPanel/MapPanel';
|
||||
import Menu from './Menu/Menu';
|
||||
import NamedLink from './NamedLink/NamedLink';
|
||||
import NamedRedirect from './NamedRedirect/NamedRedirect';
|
||||
import PageLayout from './PageLayout/PageLayout';
|
||||
import RouterProvider from './RouterProvider/RouterProvider';
|
||||
import RoutesProvider from './RoutesProvider/RoutesProvider';
|
||||
import SearchResultsPanel from './SearchResultsPanel/SearchResultsPanel';
|
||||
|
||||
export { HeroSection, NamedLink, NamedRedirect, PageLayout, RouterProvider, RoutesProvider };
|
||||
export {
|
||||
FilterPanel,
|
||||
HeroSection,
|
||||
ListingCard,
|
||||
ListingCardSmall,
|
||||
MapPanel,
|
||||
Menu,
|
||||
NamedLink,
|
||||
NamedRedirect,
|
||||
PageLayout,
|
||||
RouterProvider,
|
||||
RoutesProvider,
|
||||
SearchResultsPanel,
|
||||
};
|
||||
|
|
|
|||
36
src/containers/SearchPage/SearchPage.css
Normal file
36
src/containers/SearchPage/SearchPage.css
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
.container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.tab {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.filters {
|
||||
composes: tab;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.listings{
|
||||
composes: tab;
|
||||
}
|
||||
|
||||
.map {
|
||||
composes: tab;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.open {
|
||||
display: block;
|
||||
}
|
||||
|
|
@ -1,18 +1,97 @@
|
|||
import React from 'react';
|
||||
import React, { PropTypes } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { connect } from 'react-redux';
|
||||
import { NamedLink, PageLayout } from '../../components';
|
||||
import { find } from 'lodash';
|
||||
import { addFlashNotification } from '../../ducks/FlashNotification.ducks';
|
||||
import { addFilter } from './SearchPage.ducks';
|
||||
import css from './SearchPage.css';
|
||||
import {
|
||||
FilterPanel,
|
||||
ListingCard,
|
||||
ListingCardSmall,
|
||||
MapPanel,
|
||||
PageLayout,
|
||||
SearchResultsPanel,
|
||||
} from '../../components';
|
||||
|
||||
export const SearchPageComponent = () => (
|
||||
<PageLayout title="Search page">
|
||||
<NamedLink name="ListingPage" params={{ slug: 'Nice-studio-in-Helsinki', id: 345 }}>
|
||||
Nice studio in Helsinki
|
||||
</NamedLink>
|
||||
<br />
|
||||
<NamedLink name="LandingPage">LandingPage</NamedLink>
|
||||
</PageLayout>
|
||||
);
|
||||
const tabClasses = [
|
||||
{ name: 'filters', css: css.filters },
|
||||
{ name: 'listings', css: css.listings },
|
||||
{ name: 'map', css: css.map },
|
||||
];
|
||||
|
||||
const findTab = forTabType => {
|
||||
const foundTab = find(tabClasses, c => c.name === forTabType);
|
||||
if (!foundTab) {
|
||||
return find(tabClasses, c => c.name === 'listings');
|
||||
}
|
||||
return foundTab;
|
||||
};
|
||||
|
||||
const combinedClasses = (forTabType, currentTab) => {
|
||||
const foundTab = findTab(forTabType);
|
||||
const shouldOpenDefault = !currentTab && foundTab.name === 'listings';
|
||||
|
||||
if (foundTab.name === currentTab || shouldOpenDefault) {
|
||||
return classNames(foundTab.css, css.open);
|
||||
}
|
||||
|
||||
return foundTab.css;
|
||||
};
|
||||
|
||||
const fakeListings = [
|
||||
{
|
||||
id: 123,
|
||||
title: 'Banyan Studios',
|
||||
price: '55\u20AC / day',
|
||||
description: 'Organic Music Production in a Sustainable, Ethical and Professional Studio.',
|
||||
location: 'New York, NY \u2022 40mi away',
|
||||
review: { count: '8 reviews', rating: '4' },
|
||||
author: {
|
||||
name: 'The Stardust Collective',
|
||||
avatar: 'http://placehold.it/44x44',
|
||||
review: { rating: '4' },
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 1234,
|
||||
title: 'Pienix Studio',
|
||||
price: '80\u20AC day',
|
||||
description: 'Pienix Studio specializes in music mixing and mastering production.',
|
||||
location: 'New York, NY \u2022 6mi away',
|
||||
review: { count: '7 reviews', rating: '4' },
|
||||
author: { name: 'Juhan', avatar: 'http://placehold.it/44x44', review: { rating: '4' } },
|
||||
},
|
||||
];
|
||||
|
||||
export const SearchPageComponent = props => {
|
||||
const { tab } = props;
|
||||
return (
|
||||
<PageLayout title="Search page">
|
||||
<div className={css.container}>
|
||||
<div className={combinedClasses('filters', tab)}>
|
||||
<FilterPanel />
|
||||
</div>
|
||||
<div className={combinedClasses('listings', tab)}>
|
||||
<SearchResultsPanel>
|
||||
{fakeListings.map(l => <ListingCard key={l.id} {...l} />)}
|
||||
</SearchResultsPanel>
|
||||
</div>
|
||||
<div className={combinedClasses('map', tab)}>
|
||||
<MapPanel>
|
||||
{fakeListings.map(l => <ListingCardSmall key={l.id} {...l} />)}
|
||||
</MapPanel>
|
||||
</div>
|
||||
</div>
|
||||
</PageLayout>
|
||||
);
|
||||
};
|
||||
|
||||
SearchPageComponent.defaultProps = { tab: 'listings' };
|
||||
|
||||
const { string } = PropTypes;
|
||||
|
||||
SearchPageComponent.propTypes = { tab: string };
|
||||
|
||||
/**
|
||||
* Container functions.
|
||||
|
|
@ -25,8 +104,8 @@ const mapStateToProps = function mapStateToProps(state) {
|
|||
|
||||
const mapDispatchToProps = function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
addNotice: msg => dispatch(addFlashNotification('notice', msg)),
|
||||
addFilter: (k, v) => dispatch(addFilter(k, v)),
|
||||
onAddNotice: msg => dispatch(addFlashNotification('notice', msg)),
|
||||
onAddFilter: (k, v) => dispatch(addFilter(k, v)),
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -119,20 +119,339 @@ exports[`SearchPageComponent matches snapshot 1`] = `
|
|||
<h1>
|
||||
Search page
|
||||
</h1>
|
||||
<a
|
||||
className=""
|
||||
href="/l/Nice-studio-in-Helsinki/345"
|
||||
onClick={[Function]}
|
||||
style={Object {}}>
|
||||
Nice studio in Helsinki
|
||||
</a>
|
||||
<br />
|
||||
<a
|
||||
className=""
|
||||
href="/"
|
||||
onClick={[Function]}
|
||||
style={Object {}}>
|
||||
LandingPage
|
||||
</a>
|
||||
<div
|
||||
className={undefined}>
|
||||
<div
|
||||
className={undefined}>
|
||||
<div>
|
||||
<h1
|
||||
className={undefined}>
|
||||
Filters
|
||||
</h1>
|
||||
<a
|
||||
className=""
|
||||
href="/s/listings"
|
||||
onClick={[Function]}
|
||||
style={Object {}}>
|
||||
See studios
|
||||
</a>
|
||||
<a
|
||||
className=""
|
||||
href="/s/listings"
|
||||
onClick={[Function]}
|
||||
style={Object {}}>
|
||||
X
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="">
|
||||
<div>
|
||||
<div
|
||||
className={undefined}>
|
||||
New York, Jan 2nd – Jan 4th
|
||||
<span
|
||||
className={undefined}>
|
||||
▼
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className={undefined}>
|
||||
<div
|
||||
className={undefined}>
|
||||
<div
|
||||
className={undefined}>
|
||||
<img
|
||||
alt="Listing Title"
|
||||
className={undefined}
|
||||
src="http://placehold.it/400x300" />
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={undefined}>
|
||||
<div
|
||||
className={undefined}>
|
||||
<a
|
||||
className=""
|
||||
href="/l/Banyan-Studios/123"
|
||||
onClick={[Function]}
|
||||
style={Object {}}>
|
||||
Banyan Studios
|
||||
</a>
|
||||
<div
|
||||
className={undefined}>
|
||||
55€ / day
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={undefined}>
|
||||
Organic Music Production in a Sustainable, Ethical and Professional Studio.
|
||||
</div>
|
||||
<div
|
||||
className={undefined}>
|
||||
<div
|
||||
className={undefined}>
|
||||
New York, NY • 40mi away
|
||||
</div>
|
||||
<div
|
||||
className={undefined}>
|
||||
(
|
||||
<span>
|
||||
4
|
||||
</span>
|
||||
<span>
|
||||
/5
|
||||
</span>
|
||||
)
|
||||
|
||||
<span>
|
||||
8 reviews
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<div
|
||||
className={undefined}>
|
||||
<div
|
||||
className={undefined}>
|
||||
<img
|
||||
alt="The Stardust Collective"
|
||||
className={undefined}
|
||||
src="http://placehold.it/44x44" />
|
||||
</div>
|
||||
<div
|
||||
className={undefined}>
|
||||
<span
|
||||
className={undefined}>
|
||||
The Stardust Collective
|
||||
</span>
|
||||
<div
|
||||
className={undefined}>
|
||||
review:
|
||||
<span>
|
||||
4
|
||||
</span>
|
||||
<span>
|
||||
/5
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={undefined}>
|
||||
<div
|
||||
className={undefined}>
|
||||
<div
|
||||
className={undefined}>
|
||||
<img
|
||||
alt="Listing Title"
|
||||
className={undefined}
|
||||
src="http://placehold.it/400x300" />
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={undefined}>
|
||||
<div
|
||||
className={undefined}>
|
||||
<a
|
||||
className=""
|
||||
href="/l/Pienix-Studio/1234"
|
||||
onClick={[Function]}
|
||||
style={Object {}}>
|
||||
Pienix Studio
|
||||
</a>
|
||||
<div
|
||||
className={undefined}>
|
||||
80€ day
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={undefined}>
|
||||
Pienix Studio specializes in music mixing and mastering production.
|
||||
</div>
|
||||
<div
|
||||
className={undefined}>
|
||||
<div
|
||||
className={undefined}>
|
||||
New York, NY • 6mi away
|
||||
</div>
|
||||
<div
|
||||
className={undefined}>
|
||||
(
|
||||
<span>
|
||||
4
|
||||
</span>
|
||||
<span>
|
||||
/5
|
||||
</span>
|
||||
)
|
||||
|
||||
<span>
|
||||
7 reviews
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<div
|
||||
className={undefined}>
|
||||
<div
|
||||
className={undefined}>
|
||||
<img
|
||||
alt="Juhan"
|
||||
className={undefined}
|
||||
src="http://placehold.it/44x44" />
|
||||
</div>
|
||||
<div
|
||||
className={undefined}>
|
||||
<span
|
||||
className={undefined}>
|
||||
Juhan
|
||||
</span>
|
||||
<div
|
||||
className={undefined}>
|
||||
review:
|
||||
<span>
|
||||
4
|
||||
</span>
|
||||
<span>
|
||||
/5
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={undefined}>
|
||||
<a
|
||||
className=""
|
||||
href="/s/filters"
|
||||
onClick={[Function]}
|
||||
style={Object {}}>
|
||||
Filters
|
||||
</a>
|
||||
<a
|
||||
className=""
|
||||
href="/s/map"
|
||||
onClick={[Function]}
|
||||
style={Object {}}>
|
||||
Map
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={undefined}>
|
||||
<div>
|
||||
<div
|
||||
className={undefined}>
|
||||
Map
|
||||
</div>
|
||||
<div
|
||||
className={undefined}>
|
||||
<div
|
||||
className={undefined}>
|
||||
<div
|
||||
className={undefined}>
|
||||
<div
|
||||
className={undefined}>
|
||||
<img
|
||||
alt="Listing Title"
|
||||
className={undefined}
|
||||
src="http://placehold.it/200x150" />
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={undefined}>
|
||||
<a
|
||||
className=""
|
||||
href="/l/Banyan-Studios/123"
|
||||
onClick={[Function]}
|
||||
style={Object {}}>
|
||||
Banyan Studios
|
||||
</a>
|
||||
<div
|
||||
className={undefined}>
|
||||
(
|
||||
<span>
|
||||
4
|
||||
</span>
|
||||
<span>
|
||||
/5
|
||||
</span>
|
||||
)
|
||||
|
||||
<span>
|
||||
8 reviews
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className={undefined}>
|
||||
55€ / day
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={undefined}>
|
||||
<div
|
||||
className={undefined}>
|
||||
<div
|
||||
className={undefined}>
|
||||
<img
|
||||
alt="Listing Title"
|
||||
className={undefined}
|
||||
src="http://placehold.it/200x150" />
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={undefined}>
|
||||
<a
|
||||
className=""
|
||||
href="/l/Pienix-Studio/1234"
|
||||
onClick={[Function]}
|
||||
style={Object {}}>
|
||||
Pienix Studio
|
||||
</a>
|
||||
<div
|
||||
className={undefined}>
|
||||
(
|
||||
<span>
|
||||
4
|
||||
</span>
|
||||
<span>
|
||||
/5
|
||||
</span>
|
||||
)
|
||||
|
||||
<span>
|
||||
7 reviews
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className={undefined}>
|
||||
80€ day
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<a
|
||||
className=""
|
||||
href="/s/filters"
|
||||
onClick={[Function]}
|
||||
style={Object {}}>
|
||||
Filters
|
||||
</a>
|
||||
<a
|
||||
className=""
|
||||
href="/s/listings"
|
||||
onClick={[Function]}
|
||||
style={Object {}}>
|
||||
X
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -28,7 +28,32 @@ const RedirectLandingPage = () => <Redirect to="/" />;
|
|||
|
||||
const routesConfiguration = [
|
||||
{ pattern: '/', exactly: true, name: 'LandingPage', component: LandingPage },
|
||||
{ pattern: '/s', exactly: true, name: 'SearchPage', component: SearchPage },
|
||||
{
|
||||
pattern: '/s',
|
||||
exactly: true,
|
||||
name: 'SearchPage',
|
||||
component: SearchPage,
|
||||
routes: [
|
||||
{
|
||||
pattern: '/s/filters',
|
||||
exactly: true,
|
||||
name: 'SearchFiltersPage',
|
||||
component: props => <SearchPage {...props} tab="filters" />,
|
||||
},
|
||||
{
|
||||
pattern: '/s/listings',
|
||||
exactly: true,
|
||||
name: 'SearchListingsPage',
|
||||
component: props => <SearchPage {...props} tab="listings" />,
|
||||
},
|
||||
{
|
||||
pattern: '/s/map',
|
||||
exactly: true,
|
||||
name: 'SearchMapPage',
|
||||
component: props => <SearchPage {...props} tab="map" />,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
pattern: '/l',
|
||||
exactly: true,
|
||||
|
|
|
|||
|
|
@ -1205,6 +1205,10 @@ clap@^1.0.9:
|
|||
dependencies:
|
||||
chalk "^1.1.3"
|
||||
|
||||
classnames@^2.2.5:
|
||||
version "2.2.5"
|
||||
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.5.tgz#fb3801d453467649ef3603c7d61a02bd129bde6d"
|
||||
|
||||
clean-css@3.4.x:
|
||||
version "3.4.24"
|
||||
resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-3.4.24.tgz#89f5a5e9da37ae02394fe049a41388abbe72c3b5"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue