Commit 5d21477c authored by Jon Moore's avatar Jon Moore
Browse files

Implemented skeleton event listing page

parent 3c320a02
......@@ -34,6 +34,14 @@ export default [
loading: Loading,
}),
},
{
path: '/events-listing',
exact: true,
component: Loadable({
loader: () => import('~/pages/Listings/EventListing.page'),
loading: Loading,
}),
},
// ˄˄ Do not delete these routes ˄˄
// ********************************
// {
......
import styled from 'styled-components';
const EventCardStyled = styled.div`
background-color: #fff;
box-shadow: 0px 0px 32px rgba(0, 1, 133, 0.08);
border-radius: 12px;
font-size: 14px;
line-height: 24px;
color: #777;
.cInner {
padding: 24px 16px;
}
.cTitle {
font-weight: bold;
font-size: 16px;
line-height: 24px;
color: #333;
margin: 0;
padding-bottom: 16px;
}
.cFlex {
display: flex;
justify-content: space-between;
margin: 0 -8px;
}
.cFlexPadding {
padding: 8px;
}
.cSection {
position: relative;
margin-top: 16px;
padding: 16px 0;
&:before {
content: '';
position: absolute;
top: 0;
left: -16px;
right: -16px;
height: 1px;
background-color: #f2f2f2;
}
}
.star {
&:before {
display: inline-block;
content: '';
background-image: url('/static/img/icons/star.svg');
width: 20px;
height: 19px;
margin-left: 8px;
vertical-align: middle;
}
}
.cStats {
margin-top: 16px;
text-align: center;
}
.cMore {
display: block;
margin-top: 32px;
padding: 16px;
text-align: center;
border: 1px solid #a8bf97;
border-radius: 24px;
font-size: 16px;
color: #333;
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
`;
export default EventCardStyled;
import styled, { css } from 'styled-components';
import getIn from '~/utils/getIn';
import validateProps from '~/utils/validateProps';
const EventFiltersStyled = styled.div`
${({ theme, filtersVisible }) => {
const mqLarge = getIn(['layout', 'mediaQueries', 'large'], theme);
const visuallyHidden = getIn(['patterns', 'visuallyHidden'], theme);
validateProps('TreatmentsFiltersStyled', { mqLarge, visuallyHidden });
return css`
@media only screen and (min-width: ${mqLarge}) {
padding-right: 32px;
}
.filterGroup:not(:first-child) {
margin-top: 24px;
}
.filterGroup .open .feToggle {
border-radius: 16px 16px 0 0;
&:after {
transform: translateY(-50%) rotate(180deg);
}
}
.feLabel {
${visuallyHidden}
}
.feToggle {
position: relative;
padding-right: 32px;
display: block;
width: 100%;
padding: 12px 16px;
background: rgba(163, 183, 148, 0.2);
border-radius: 16px;
border: none;
font-size: 16px;
line-height: 24px;
color: #333333;
text-align: left;
&:after {
content: '';
background-image: url('/static/img/icons/chevron_down.svg');
width: 10px;
height: 5px;
position: absolute;
right: 16px;
top: 50%;
transform: translateY(-50%);
}
}
.toggleFiltersWrap {
text-align: right;
@media only screen and (min-width: ${mqLarge}) {
display: none;
}
}
.toggleFilters {
border: none;
margin-top: 24px;
background: transparent;
font-size: 14px;
color: #879b79;
text-decoration: underline;
&:after {
content: '';
display: inline-block;
margin-left: 8px;
background-image: url('/static/img/icons/chevron_down_green.svg');
width: 8px;
height: 5px;
${filtersVisible && 'transform:rotate(180deg)'}
}
}
${!filtersVisible && '.filterGroup:nth-child(1n+4) {display:none}'};
@media only screen and (min-width: ${mqLarge}) {
${!filtersVisible && '.filterGroup:nth-child(1n+4) {display:block}'};
}
.feList {
border-top: 1px solid #fff;
list-style: none;
margin: 0;
padding: 24px 16px;
background: rgba(163, 183, 148, 0.2);
border-radius: 0 0 16px 16px;
}
.feItem:not(:first-child) {
margin-top: 24px;
}
.feItemInput {
${visuallyHidden};
}
.feItemInput:checked + .feItemLabel {
font-weight: bold;
}
`;
}};
`;
export default EventFiltersStyled;
import styled, { css } from 'styled-components';
import validateProps from '~/utils/validateProps';
import getIn from '~/utils/getIn';
const EventListingStyled = styled.div`
${({ theme }) => {
const container = getIn(['patterns', 'container'], theme);
const gutter = getIn(['layout', 'gutter'], theme);
const mqXSmall = getIn(['layout', 'mediaQueries', 'xsmall'], theme);
const mqMedium = getIn(['layout', 'mediaQueries', 'medium'], theme);
const mqLarge = getIn(['layout', 'mediaQueries', 'large'], theme);
const mqXLarge = getIn(['layout', 'mediaQueries', 'xlarge'], theme);
validateProps('EventListingStyled', {
container,
gutter,
mqXSmall,
mqMedium,
mqLarge,
mqXLarge,
});
return css`
.container {
${container}
}
.flexWrap {
margin: 64px -${gutter} 0;
@media only screen and (min-width: ${mqLarge}) {
display: flex;
align-items: flex-start;
justify-content: space-between;
}
}
.flexColMain {
margin-top: 40px;
flex-basis: ${(100 / 12) * 9}%;
padding-bottom: 70px;
@media only screen and (min-width: ${mqLarge}) {
margin-top: 0;
}
}
.flexColAside {
flex-basis: ${(100 / 12) * 3}%;
}
.flexColPadding {
padding: 0 ${gutter};
}
.resultsInfo span {
font-weight: bold;
}
.results {
margin: -24px -24px 0;
@media only screen and (min-width: ${mqXLarge}) {
margin: -32px -32px 0;
}
@media only screen and (min-width: ${mqXSmall}) {
display: flex;
flex-wrap: wrap;
}
}
.card {
flex-basis: 50%;
@media only screen and (min-width: ${mqMedium}) {
flex-basis: ${(100 / 3) * 1}%;
}
}
.cardPadding {
padding: 24px;
@media only screen and (min-width: ${mqXLarge}) {
padding: 32px;
}
}
.downloadWrap {
text-align: right;
}
.download {
background: #c3deaf;
border-radius: 24px;
text-align: center;
padding: 16px;
width: 100%;
display: inline-block;
margin-top: 40px;
border: none;
font-size: 16px;
line-height: 24px;
color: #333;
&:first-child {
margin-right: 20px;
}
@media only screen and (min-width: ${mqLarge}) {
width: auto;
}
span {
&:before {
display: inline-block;
vertical-align: middle;
margin: 0 8px;
content: '';
background-image: url('/static/img/icons/download.svg');
width: 16px;
height: 20px;
}
}
}
`;
}};
`;
export default EventListingStyled;
import React from 'react';
import PropTypes from 'prop-types';
import { Link } from 'react-router-dom';
import EventCardStyled from '../components.styled/EventCard.styled';
const EventCard = ({ title, description, image, uri, date, className }) => {
return (
<EventCardStyled className={className}>
<div className="cInner">
{image && <img src={image.asset.sys.uri} alt={image.altText} />}
<div className="cTitle">
<Link to={uri} title={title}>
{title}
</Link>
</div>
<p>{description}</p>
<p>{date}</p>
</div>
</EventCardStyled>
);
};
EventCard.propTypes = {
className: PropTypes.string,
title: PropTypes.string,
description: PropTypes.string,
image: PropTypes.object,
uri: PropTypes.string,
date: PropTypes.object,
};
export default EventCard;
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import EventFiltersStyled from '../components.styled/EventFilters.styled';
import FilterEntryDropdown from '~/features/listings/components/FilterEntryDropdown';
const EventFilters = ({ className, filters, updateFilters }) => {
const [filtersVisible, toggleFilters] = useState(false);
const renderFilter = filterGroup => {
switch (filterGroup.type) {
case 'entry': {
return (
<FilterEntryDropdown
className="tfCategory"
entries={filterGroup.params.entries}
id={filterGroup.id}
label={filterGroup.label}
update={updateFilters}
value={filterGroup.value}
/>
);
}
case 'list': {
return (
<FilterEntryDropdown
className="tfCategory"
entries={filterGroup.params.options}
id={filterGroup.id}
label={filterGroup.label}
update={updateFilters}
type={'list'}
value={filterGroup.value}
/>
);
}
default:
return;
}
};
return (
<EventFiltersStyled className={className} filtersVisible={filtersVisible}>
<div className="tfInner">
{filters &&
filters.map((filterGroup, idx) => {
return (
<div key={idx} className="filterGroup">
{renderFilter(filterGroup)}
</div>
);
})}
<div className="toggleFiltersWrap">
<button
onClick={() => toggleFilters(!filtersVisible)}
className="toggleFilters"
>
Additional filters
</button>
</div>
</div>
</EventFiltersStyled>
);
};
EventFilters.propTypes = {
className: PropTypes.string,
clearFilters: PropTypes.func,
filters: PropTypes.array,
updateFilters: PropTypes.func,
};
export default EventFilters;
import React from 'react';
import PropTypes from 'prop-types';
import { withRouter } from 'react-router-dom';
import EventFilters from './EventFilters';
import EventListingStyled from '../components.styled/EventListing.styled';
import EventCard from './EventCard';
const EventListing = ({
className,
filters,
paging,
results,
updateFilters,
}) => {
let resultsInfo = null;
if (paging && paging.totalCount > 0) {
const start = paging.pageIndex * paging.pageSize + 1;
let end = paging.pageSize;
if (end > paging.totalCount) end = paging.totalCount;
resultsInfo = `Displaying ${start}-${end} of ${paging.totalCount} results`;
}
return (
<EventListingStyled className={className}>
<div className="container">
<h1 className="pageTitle">Example listing title</h1>
{paging && (
<div
className="resultsInfo"
dangerouslySetInnerHTML={{ __html: resultsInfo }}
/>
)}
<div className="flexWrap">
<aside className="flexColAside">
<div className="flexColPadding">
<EventFilters filters={filters} updateFilters={updateFilters} />
</div>
</aside>
<main className="flexColMain">
<div className="flexColPadding">
<div className="results">
{results &&
results.map((entry, idx) => {
return (
<div key={idx} className="card">
<div className="cardPadding">
<EventCard
title={entry.entryTitle}
description={entry.entryDescription}
image={entry.image}
uri={entry.sys && entry.sys.uri}
/>
</div>
</div>
);
})}
{!results || (results.length < 1 && <p>No results found.</p>)}
</div>
</div>
</main>
</div>
</div>
</EventListingStyled>
);
};
EventListing.propTypes = {
className: PropTypes.string,
filters: PropTypes.arrayOf(PropTypes.object),
paging: PropTypes.object,
results: PropTypes.arrayOf(PropTypes.object),
updateFilters: PropTypes.func,
};
export default withRouter(EventListing);
import React from 'react';
import { storiesOf } from '@storybook/react';
import { text } from '@storybook/addon-knobs';
import EventCard from '~/features/events/components/EventCard';
storiesOf('Features | Event', module).add(
'Event card',
() => {
return (
<EventCard
title={text('Title', 'Event card title')}
description="Event card description"
uri="/example-content/example-article"
/>
);
},
{
knobs: {
escapeHTML: false,
},
}
);
import React from 'react';
import PropTypes from 'prop-types';
import MainLayout from '~/layouts/Main.layout';
import EventListing from '~/features/events/components/EventsListing';
import ListingContainer from '~/features/listings/containers/Listing.container';
const EventListingPage = () => {
return (
<MainLayout>
<ListingContainer
config={{
listingType: 'event',
contentTypeIds: ['event'],
fields: [
'entryTitle',
'entryDescription',
'image',
'eventCategories',
],
pageSize: 4,
orderBy: {
field: 'datesAndTimes.from',
direction: 'asc',
},
filters: [
{
label: 'Categories',
type: 'entry',
fields: ['eventCategories[]'],
params: {
contentTypeIds: ['eventCategories'],
<