mirror of
https://github.com/kingomarnajjar/flex-template-web.git
synced 2026-07-26 06:47:17 +10:00
ListingCard for SearchResultsPanel and ListingCardSmall for MapPanel
This commit is contained in:
parent
acf85d9af6
commit
1fdccf7f71
8 changed files with 419 additions and 0 deletions
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>
|
||||
`;
|
||||
Loading…
Add table
Reference in a new issue