Skip to content

Instantly share code, notes, and snippets.

@simaob
Created March 27, 2020 20:43
Show Gist options
  • Save simaob/999fcbf072f6cb29f3da8efb71f8a278 to your computer and use it in GitHub Desktop.
Save simaob/999fcbf072f6cb29f3da8efb71f8a278 to your computer and use it in GitHub Desktop.
import React, { Component, Fragment } from 'react';
import PropTypes from 'prop-types';
import qs from 'qs';
import SearchFilter from '../../SearchFilter';
/* import TimeRangeFilter from '../../TimeRangeFilter'; */
function getQueryFilters() {
return qs.parse(window.location.search.slice(1));
}
class ClimateTargets extends Component {
constructor(props) {
super(props);
const {
climate_targets,
count
} = this.props;
this.state = {
climate_targets,
count,
offset: 0,
activeGeoFilter: {},
activeTimeRangeFilter: {},
activeTypesFilter: {},
activeSectorsFilter: {},
activeTargetYearsFilter: {},
isMoreSearchOptionsVisible: false
};
this.isMobile = window.innerWidth < 1024;
this.geoFilter = React.createRef();
this.sectorsFilter = React.createRef();
this.targetYearsFilter = React.createRef();
this.timeRangeFilter = React.createRef();
this.typesFilter = React.createRef();
}
getQueryString(extraParams = {}) {
const {
activeGeoFilter,
activeTypesFilter,
activeTimeRangeFilter,
activeSectorsFilter,
activeTargetYearsFilter
} = this.state;
const params = {
...getQueryFilters(),
...activeTimeRangeFilter,
...activeGeoFilter,
...activeTypesFilter,
...activeSectorsFilter,
...activeTargetYearsFilter,
...extraParams
};
return qs.stringify(params, { arrayFormat: 'brackets' });
}
handleLoadMore = () => {
const { climate_targets } = this.state;
this.setState({ offset: climate_targets.length }, this.fetchData.bind(this));
};
filterList = (activeFilterName, filterParams) => {
this.setState({[activeFilterName]: filterParams, offset: 0}, this.fetchData.bind(this));
};
fetchData() {
const { offset } = this.state;
const newQs = this.getQueryString({ offset });
fetch(`/cclow/climate_targets.json?${newQs}`).then((response) => {
response.json().then((data) => {
if (response.ok) {
if (offset > 0) {
this.setState(({ climate_targets }) => ({
climate_targets: climate_targets.concat(data.climate_targets)
}));
} else {
this.setState({climate_targets: data.climate_targets, count: data.count});
}
}
});
});
}
renderPageTitle() {
const qFilters = getQueryFilters();
const filterText = qFilters.q || (qFilters.recent && 'recent additions');
if (filterText) {
return (
<h5 className="search-title">
Search results: <strong>{filterText}</strong> in Climate Targets
</h5>
);
}
return (<h5>All Climate Targets</h5>);
}
renderTags = () => {
const {
activeGeoFilter,
activeTimeRangeFilter,
activeTypesFilter,
activeSectorsFilter,
activeTargetYearsFilter
} = this.state;
const {
geo_filter_options: geoFilterOptions,
types_filter_options: typesFilterOptions,
sectors_options: sectorsOptions,
target_years_options: targetYearsOptions
} = this.props;
if (Object.keys(activeGeoFilter).length === 0
&& Object.keys(activeTypesFilter).length === 0
&& Object.keys(activeTimeRangeFilter).length === 0
&& Object.keys(activeSectorsFilter).length === 0
&& Object.keys(activeTargetYearsFilter).length === 0) return null;
return (
<div className="filter-tags tags">
{this.renderTagsGroup(activeGeoFilter, geoFilterOptions, 'geoFilter')}
{this.renderTagsGroup(activeSectorsFilter, sectorsOptions, 'sectorsFilter')}
{this.renderTagsGroup(activeTypesFilter, typesFilterOptions, 'typesFilter')}
{this.renderTagsGroup(activeTargetYearsFilter, targetYearsOptions, 'targetYearsFilter')}
{this.renderTimeRangeTags(activeTimeRangeFilter)}
</div>
);
};
renderTimeRangeTags = (value) => (
<Fragment>
{value.from_date && (
<span key="tag-time-range-from" className="tag">
From {value.from_date}
<button
type="button"
onClick={() => this.timeRangeFilter.current.handleChange({from_date: null})}
className="delete"
/>
</span>
)}
{value.to_date && (
<span key="tag-time-range-to" className="tag">
To {value.to_date}
<button
type="button"
onClick={() => this.timeRangeFilter.current.handleChange({to_date: null})}
className="delete"
/>
</span>
)}
</Fragment>
);
renderTagsGroup = (activeTags, options, filterEl) => (
<Fragment>
{Object.keys(activeTags).map((keyBlock) => (
activeTags[keyBlock].map((key, i) => (
<span key={`tag_${keyBlock}_${i}`} className="tag">
{options.filter(item => item.field_name === keyBlock)[0].options.filter(l => l.value === key)[0].label}
<button type="button" onClick={() => this[filterEl].current.handleCheckItem(keyBlock, key)} className="delete" />
</span>
))
))}
</Fragment>
);
renderFilters = () => {
const {
geo_filter_options: geoFilterOptions,
types_filter_options: typesFilterOptions,
sectors_options: sectorsOptions,
target_years_options: targetYearsOptions
} = this.props;
return (
<Fragment>
<SearchFilter
ref={this.geoFilter}
filterName="Regions and countries"
params={geoFilterOptions}
onChange={(event) => this.filterList('activeGeoFilter', event)}
/>
<SearchFilter
ref={this.sectorsFilter}
filterName="Sectors"
params={sectorsOptions}
onChange={(event) => this.filterList('activeSectorsFilter', event)}
/>
<SearchFilter
ref={this.typesFilter}
filterName="Target types"
params={typesFilterOptions}
onChange={(event) => this.filterList('activeTypesFilter', event)}
/>
<SearchFilter
ref={this.targetYearsFilter}
filterName="Target years"
params={targetYearsOptions}
onChange={(event) => this.filterList('activeTargetYearsFilter', event)}
/>
</Fragment>
);
}
renderMoreOptions = () => {
const {isMoreSearchOptionsVisible} = this.state;
return (
<Fragment>
{!isMoreSearchOptionsVisible && (
<button
type="button"
onClick={() => this.setState({isMoreSearchOptionsVisible: true})}
className="more-options"
>
+ Show more search options
</button>
)}
<div className={isMoreSearchOptionsVisible ? '' : 'hidden'}>
<button
type="button"
onClick={() => this.setState({isMoreSearchOptionsVisible: false})}
className="more-options"
>
- Show fewer search options
</button>
{this.renderFilters()}
</div>
</Fragment>
);
}
render() {
const {climate_targets, count} = this.state;
const hasMore = climate_targets.length < count;
const downloadResultsLink = `/cclow/climate_targets.csv?${this.getQueryString()}`;
return (
<Fragment>
<div className="cclow-geography-page">
<div className="container">
<div className="flex-container">
{this.renderPageTitle()}
</div>
{this.isMobile && (<div className="filter-column">{this.renderMoreOptions()}</div>)}
<hr />
<div className="columns">
{!this.isMobile && (
<div className="column is-one-quarter filter-column">
<div className="search-by">Narrow this search by</div>
{this.renderFilters()}
</div>
)}
<main className="column is-three-quarters">
<div className="columns pre-content">
<span className="column is-half">Showing {count} results</span>
<span className="column is-half download-link is-hidden-touch">
<a className="download-link" href={downloadResultsLink}>Download results (.csv)</a>
</span>
</div>
{this.renderTags()}
<ul className="content-list">
{climate_targets.map((target, i) => (
<Fragment key={i}>
<li className="content-item">
<h5 className="title" dangerouslySetInnerHTML={{__html: target.link}} />
<div className="meta">
{target.geography && (
<Fragment>
<a href={target.geography_path}>
<img src={`/images/flags/${target.geography.iso}.svg`} alt="" />
{target.geography.name}
</a>
</Fragment>
)}
<div>{target.target_tags && target.target_tags.join(' | ')}</div>
</div>
</li>
</Fragment>
))}
</ul>
{hasMore && (
<div className={`column load-more-container${!this.isMobile ? ' is-offset-5' : ''}`}>
<button type="button" className="button is-primary load-more-btn" onClick={this.handleLoadMore}>
Load 10 more entries
</button>
</div>
)}
</main>
</div>
</div>
</div>
</Fragment>
);
}
}
ClimateTargets.defaultProps = {
count: 0,
geo_filter_options: [],
types_filter_options: [],
sectors_options: [],
target_years_options: []
};
ClimateTargets.propTypes = {
climate_targets: PropTypes.array.isRequired,
count: PropTypes.number,
geo_filter_options: PropTypes.array,
types_filter_options: PropTypes.array,
sectors_options: PropTypes.array,
target_years_options: PropTypes.array
};
export default ClimateTargets;
import React, { Fragment, Component } from 'react';
import PropTypes from 'prop-types';
import sortBy from 'lodash/sortBy';
import search from '../../assets/images/icons/search.svg';
import minus from '../../assets/images/icons/dark-minus.svg';
import plus from '../../assets/images/icons/dark-plus.svg';
class SearchFilter extends Component {
constructor(props) {
super(props);
this.state = {
isShowOptions: false,
selectedList: {},
searchValue: ''
};
this.optionsContainer = React.createRef();
}
componentDidMount() {
document.addEventListener('mousedown', this.handleClickOutside);
return () => {
document.removeEventListener('mousedown', this.handleClickOutside);
};
}
setShowOptions = value => this.setState({isShowOptions: value});
setSelectedList = value => this.setState({selectedList: value});
setSearchValue = value => this.setState({searchValue: value});
handleCloseOptions = () => {
this.setSearchValue('');
this.setShowOptions(false);
};
handleClickOutside = (event) => {
if (this.optionsContainer.current && !this.optionsContainer.current.contains(event.target)) {
this.handleCloseOptions();
}
};
handleCheckItem = (blockName, value) => {
const {selectedList} = this.state;
const {onChange} = this.props;
const blocks = Object.assign({}, selectedList);
if ((blocks[blockName] || []).includes(value)) {
blocks[blockName] = blocks[blockName].filter(item => item !== value);
if (blocks[blockName].length === 0) delete blocks[blockName];
} else {
if (!blocks[blockName]) blocks[blockName] = [];
blocks[blockName].push(value);
}
onChange(blocks);
this.setSelectedList(blocks);
};
handleSearchInput = e => this.setSearchValue(e.target.value);
itemIsSelected = (fieldName, value) => this.state.selectedList[fieldName] && this.state.selectedList[fieldName].includes(value);
renderBlocksList = (blocks) => {
const options = blocks.map((o) => {
const optionsWithFieldName = o.options.map((el) => ({ ...el, fieldName: o.field_name }));
return optionsWithFieldName;
});
const sortedOptions = sortBy(options.flat(), 'label');
return (
<Fragment>
<ul>
{sortedOptions.map(option => (
<li key={option.value} onClick={() => this.handleCheckItem(option.fieldName, option.value)}>
<input type="checkbox" hidden checked={this.itemIsSelected(option.fieldName, option.value) || false} onChange={() => {}} />
<div className={`${this.itemIsSelected(option.fieldName, option.value) ? 'checked' : 'unchecked'} select-checkbox`}>
{this.itemIsSelected(option.fieldName, option.value) && <i className="fa fa-check" />}
</div>
<label>{option.label}</label>
</li>
))}
</ul>
</Fragment>
);
}
renderBlockList = (block, index) => {
const {options, field_name: fieldName} = block;
if ((options || []).length === 0) return null;
return (
<Fragment key={fieldName}>
{ index !== 0 && <hr /> }
<ul>
{options.map(option => (
<li key={option.value} onClick={() => this.handleCheckItem(fieldName, option.value)}>
<input type="checkbox" hidden checked={this.itemIsSelected(fieldName, option.value) || false} onChange={() => {}} />
<div className={`${this.itemIsSelected(fieldName, option.value) ? 'checked' : 'unchecked'} select-checkbox`}>
{this.itemIsSelected(fieldName, option.value) && <i className="fa fa-check" />}
</div>
<label>{option.label}</label>
</li>
))}
</ul>
</Fragment>
);
};
renderOptions = () => {
const {searchValue} = this.state;
const {filterName, params, isSearchable} = this.props;
const listBlocks = [];
for (let i = 0; i < params.length; i += 1) {
listBlocks[i] = Object.assign({}, params[i]);
if (searchValue) {
listBlocks[i].options = listBlocks[i]
.options.concat().filter(item => item.label.toLowerCase().includes(searchValue.toLowerCase()));
}
}
return (
<div className="options-container" ref={this.optionsContainer}>
<div className="select-field" onClick={this.handleCloseOptions}>
<span>{filterName}</span><span className="toggle-indicator"><img src={minus} alt="" /></span>
</div>
<div>
{isSearchable && (
<div className="search-input-container">
<input id="search-input" type="text" onChange={this.handleSearchInput} />
<label htmlFor="search-input">
<img src={search} />
</label>
</div>
)}
<div className="options-list">
{listBlocks.length > 1 ? (
this.renderBlocksList(listBlocks)
) : (
listBlocks.map((blockList, i) => this.renderBlockList(blockList, i))
)}
</div>
</div>
</div>
);
};
isEmpty = () => {
const {params} = this.props;
for (let i = 0; i < params.length; i = 1) {
if ((params[i].options || []).length !== 0) return false;
}
return true;
};
render() {
const {selectedList, isShowOptions} = this.state;
const {filterName} = this.props;
if (this.isEmpty()) return null;
let selectedCount = 0;
Object.values(selectedList).forEach(list => { selectedCount += list.length; });
return (
<Fragment>
<div className="filter-container">
<div className="control-field" onClick={() => this.setShowOptions(true)}>
<div className="select-field">
<span>{filterName}</span><span className="toggle-indicator"><img src={plus} alt="" /></span>
</div>
{selectedCount !== 0 && <div className="selected-count">{selectedCount} selected</div>}
</div>
{ isShowOptions && this.renderOptions()}
</div>
</Fragment>
);
}
}
SearchFilter.defaultProps = {
onChange: () => {},
isSearchable: true
};
SearchFilter.propTypes = {
filterName: PropTypes.string.isRequired,
params: PropTypes.array.isRequired,
onChange: PropTypes.func,
isSearchable: PropTypes.bool
};
export default SearchFilter;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment