fronts-client/src/components/feed/CapiSearchContainer.tsx (386 lines of code) (raw):
import React from 'react';
import { connect } from 'react-redux';
import { styled, theme } from 'constants/theme';
import SearchInput, { SearchInputState, initState } from './SearchInput';
import Feed from './Feed';
import { RadioButton, RadioGroup } from '../inputs/RadioButtons';
import type { State } from 'types/State';
import {
liveSelectors,
previewSelectors,
fetchLive,
fetchPreview,
prefillSelectors,
hidePrefills,
} from 'bundles/capiFeedBundle';
import { getTodayDate } from 'util/getTodayDate';
import { getIdFromURL } from 'util/CAPIUtils';
import { Dispatch } from 'types/Store';
import debounce from 'lodash/debounce';
import Pagination from './Pagination';
import { IPagination } from 'lib/createAsyncResourceBundle';
import ShortVerticalPinline from 'components/layout/ShortVerticalPinline';
import { DEFAULT_PARAMS } from 'services/faciaApi';
import ScrollContainer from '../ScrollContainer';
import ClipboardHeader from 'components/ClipboardHeader';
import ContainerHeading from 'components/typography/ContainerHeading';
import { ClearIcon } from 'components/icons/Icons';
import Button from 'components/inputs/ButtonDefault';
import { selectIsPrefillMode } from 'selectors/feedStateSelectors';
import { feedArticlesPollInterval } from 'constants/polling';
import { SearchTitle } from './SearchTitle';
import { SearchResultsHeadingContainer } from './SearchResultsHeadingContainer';
interface CapiSearchContainerProps {
fetchLive: (params: object, isResource: boolean) => void;
fetchPreview: (params: object, isResource: boolean) => void;
hidePrefills: () => void;
isPrefillMode: boolean;
livePagination: IPagination | null;
previewPagination: IPagination | null;
liveLoading: boolean;
previewLoading: boolean;
prefillLoading: boolean;
}
interface CapiSearchContainerState {
capiFeedIndex: number;
displaySearchFilters: boolean;
inputState: SearchInputState;
displayPrevResults: boolean;
sortByParam: string;
}
const RefreshButton = styled.button`
padding-left: 0;
appearance: none;
border: none;
background: transparent;
color: ${theme.base.colors.text};
cursor: pointer;
font-family: inherit;
font-size: 13px;
font-weight: bold;
outline: none;
&:hover {
color: ${theme.base.colors.buttonFocused};
}
&:disabled {
color: ${theme.base.colors.textMuted};
}
`;
const FeedsContainerWrapper = styled.div`
height: 100%;
`;
const PaginationContainer = styled.div`
margin-left: auto;
`;
const ResultsContainer = styled.div`
margin-right: 10px;
`;
const FixedContentContainer = styled.div`
margin-bottom: 5px;
`;
const Sorters = styled.div`
display: flex;
flex-direction: column;
`;
const TopOptions = styled.div`
display: flex;
flex-direction: row;
`;
const SortByContainer = styled.div`
flex: 1 0 auto;
display: flex;
flex-direction: row;
margin-top: 5px;
font-size: 12px;
> label {
margin-right: 15px;
}
`;
const PrefillNoticeContainer = styled.div`
margin-bottom: 10px;
display: flex;
justify-content: space-between;
align-items: center;
`;
const PrefillNotice = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
flex-grow: 2;
`;
const PrefillCloseButton = styled(Button)`
color: #fff;
padding: 0 5px;
display: flex;
align-items: center;
`;
const getCapiFieldsToShow = (isPreview: boolean) => {
const defaultFieldsToShow = DEFAULT_PARAMS['show-fields']
.split(',')
.filter((f) => f !== 'scheduledPublicationDate')
.join(',');
if (!isPreview) {
return defaultFieldsToShow;
}
return defaultFieldsToShow + ',scheduledPublicationDate';
};
const getParams = (
query: string,
{
tags,
sections,
desks,
ratings,
toDate: to,
fromDate: from,
}: SearchInputState,
isPreview: boolean,
sortByParam: string,
) => ({
q: query,
tag: [...tags, ...desks].join(','),
section: sections.join(','),
'star-rating': ratings.join('|'),
'to-date': to && to.format('YYYY-MM-DD'),
'page-size': '20',
'show-elements': 'image',
'show-tags': 'all',
'show-fields': getCapiFieldsToShow(isPreview),
'show-atoms': 'media',
'show-blocks': 'main',
...(isPreview
? { 'order-by': 'oldest', 'from-date': getTodayDate() }
: {
'order-by': 'newest',
'order-date': sortByParam,
'from-date': from && from.format('YYYY-MM-DD'),
}),
});
class CapiSearchContainer extends React.Component<
CapiSearchContainerProps,
CapiSearchContainerState
> {
public state = {
capiFeedIndex: 0,
displaySearchFilters: false,
inputState: initState,
displayPrevResults: false,
sortByParam: 'published',
};
private interval: null | number = null;
constructor(props: CapiSearchContainerProps) {
super(props);
this.debouncedRunSearchAndRestartPolling = debounce(
() => this.runSearchAndRestartPolling(),
750,
);
}
public componentDidMount() {
this.runSearchAndRestartPolling();
}
public componentWillUnmount() {
this.stopPolling();
}
public handleParamsUpdate = (state: SearchInputState) => {
this.setState(
{
inputState: state,
},
() => this.debouncedRunSearchAndRestartPolling(),
);
};
public updateDisplaySearchFilters = (newValue: boolean) =>
this.setState({
displaySearchFilters: newValue,
});
public handleFeedClick = (index: number) =>
this.setState(
{
capiFeedIndex: index,
},
this.runSearch,
);
public sortResultsBy = (event: React.ChangeEvent<HTMLSelectElement>) =>
this.setState({ sortByParam: event.target.value }, this.runSearch);
public get isLoading() {
return (
(this.state.capiFeedIndex === 0 && this.props.liveLoading) ||
(this.state.capiFeedIndex === 1 && this.props.previewLoading)
);
}
public get isLive() {
return this.state.capiFeedIndex === 0;
}
public renderPrefillFixedContent = () => {
return (
<PrefillNoticeContainer>
<PrefillNotice>
<ContainerHeading>Suggested Articles</ContainerHeading>
<PrefillCloseButton size="l" onClick={this.props.hidePrefills}>
<ClearIcon size="xl" />
</PrefillCloseButton>
</PrefillNotice>
<ClipboardHeader />
</PrefillNoticeContainer>
);
};
public renderSearchFixedContent = (displaySearchFilters: boolean) => {
const pagination = this.getPagination();
const hasPages = !!(pagination && pagination.totalPages > 1);
return (
<React.Fragment>
<SearchInput
updateDisplaySearchFilters={this.updateDisplaySearchFilters}
displaySearchFilters={this.state.displaySearchFilters}
onUpdate={this.handleParamsUpdate}
showReviewSearch={false}
rightHandContainer={<ClipboardHeader />}
/>
<FixedContentContainer>
<SearchResultsHeadingContainer>
<div>
{displaySearchFilters ? (
<SearchTitle>
{'Results'}
<ShortVerticalPinline />
</SearchTitle>
) : (
<>
<SearchTitle>
{'Latest'}
<ShortVerticalPinline />
</SearchTitle>
<RefreshButton
disabled={this.isLoading}
onClick={() => this.runSearchAndRestartPolling()}
>
{this.isLoading ? 'Loading' : 'Refresh'}
</RefreshButton>
</>
)}
</div>
<Sorters>
<TopOptions>
<RadioGroup>
<RadioButton
checked={this.state.capiFeedIndex === 0}
onChange={() => this.handleFeedClick(0)}
label="Live"
inline
name="capiFeed"
/>
<RadioButton
checked={this.state.capiFeedIndex === 1}
onChange={() => this.handleFeedClick(1)}
label="Draft"
inline
name="capiFeed"
/>
</RadioGroup>
{pagination && hasPages && (
<PaginationContainer>
<Pagination
pageChange={this.handlePageChange}
currentPage={pagination.currentPage}
totalPages={pagination.totalPages}
/>
</PaginationContainer>
)}
</TopOptions>
<SortByContainer>
<label htmlFor="sort-results">Sort by:</label>
<select
id="sort-results"
onChange={this.sortResultsBy}
value={this.state.sortByParam}
>
<option value="first-publication">First published</option>
<option value="published">Latest published</option>
</select>
</SortByContainer>
</Sorters>
</SearchResultsHeadingContainer>
</FixedContentContainer>
</React.Fragment>
);
};
public render() {
const { isPrefillMode } = this.props;
return (
<FeedsContainerWrapper>
<ScrollContainer
fixed={
isPrefillMode
? this.renderPrefillFixedContent()
: this.renderSearchFixedContent(this.state.displaySearchFilters)
}
>
<ResultsContainer>
<Feed isLive={this.isLive} />
</ResultsContainer>
</ScrollContainer>
</FeedsContainerWrapper>
);
}
private getPagination() {
const { livePagination, previewPagination } = this.props;
return this.isLive ? livePagination : previewPagination;
}
private handlePageChange = (page: number) => {
if (page > 1) {
this.runSearch(page);
this.stopPolling();
} else {
this.runSearchAndRestartPolling();
}
};
private runSearch(page: number = 1) {
const { inputState } = this.state;
const { capiFeedIndex } = this.state;
const maybeArticleId = getIdFromURL(inputState.query);
const searchTerm = maybeArticleId ? maybeArticleId : inputState.query;
const isLive = capiFeedIndex === 0;
const fetch = isLive ? this.props.fetchLive : this.props.fetchPreview;
fetch(
{
...getParams(searchTerm, inputState, !isLive, this.state.sortByParam),
page,
},
!!maybeArticleId,
);
}
private runSearchAndRestartPolling() {
this.stopPolling();
this.interval = window.setInterval(
() => this.runSearch(),
feedArticlesPollInterval,
);
this.runSearch();
}
private stopPolling() {
if (this.interval) {
window.clearInterval(this.interval);
}
}
private debouncedRunSearchAndRestartPolling = () => {};
}
const mapStateToProps = (state: State) => ({
isPrefillMode: selectIsPrefillMode(state),
liveLoading: liveSelectors.selectIsLoading(state),
previewLoading: previewSelectors.selectIsLoading(state),
prefillLoading: prefillSelectors.selectIsLoading(state),
livePagination: liveSelectors.selectPagination(state),
previewPagination: previewSelectors.selectPagination(state),
prefillPagination: prefillSelectors.selectPagination(state),
});
const mapDispatchToProps = (dispatch: Dispatch) => ({
fetchLive: (params: object, isResource: boolean) =>
dispatch(fetchLive(params, isResource)),
fetchPreview: (params: object, isResource: boolean) =>
dispatch(fetchPreview(params, isResource)),
hidePrefills: () => dispatch(hidePrefills()),
});
export default connect(
mapStateToProps,
mapDispatchToProps,
)(CapiSearchContainer);