bookbrainz/bookbrainz-site

View on GitHub
src/client/components/pages/search.tsx

Summary

Maintainability
B
6 hrs
Test Coverage
/*
 * Copyright (C) 2015  Ohm Patel
 *               2016  Sean Burke
 *               2018  Nicolas Pelleiter
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 */

import * as React from 'react';
import CallToAction from './parts/call-to-action';
import PagerElement from './parts/pager';
import PropTypes from 'prop-types';
import SearchField from './parts/search-field';
import SearchResults from './parts/search-results';


type Props = {
    entityTypes: any[],
    from?: number,
    initialResults?: any[],
    nextEnabled: boolean,
    query?: string,
    resultsPerPage?: number,
    type?: string,
    user: Record<string, unknown>
};

type State = {
    query: string | null | undefined;
    results: any[];
    type: string | null | undefined;
};

class SearchPage extends React.Component<Props, State> {
    static displayName = 'SearchPage';

    static propTypes = {
        entityTypes: PropTypes.array.isRequired,
        from: PropTypes.number,
        initialResults: PropTypes.array,
        nextEnabled: PropTypes.bool.isRequired,
        query: PropTypes.string,
        resultsPerPage: PropTypes.number,
        type: PropTypes.string,
        user: PropTypes.object.isRequired
    };

    static defaultProps = {
        from: 0,
        initialResults: [],
        query: '',
        resultsPerPage: 20,
        type: null
    };

    /**
     * Initializes component state to default values and binds class
     * methods to proper context so that they can be directly invoked
     * without explicit binding.
     *
     * @param {object} props - Properties object passed down from parents.
     */
    constructor(props) {
        super(props);

        this.state = {
            query: props.query,
            results: props.initialResults,
            type: props.type
        };

        this.paginationUrl = './search/search';
        this.pagerElementRef = React.createRef();
    }

    paginationUrl: string;

    pagerElementRef: React.RefObject<PagerElement>;

    /**
     * Gets user text query from the browser's URL search parameters and
     * sets it in the state to be passed down to SearchField and Pager components
     *
     * @param {string} query - Query string entered by user.
     * @param {string} type - Entity type selected from dropdown
     */
    handleSearch = (query: string, type: string) => {
        if (query === this.state.query && type === this.state.type && this.pagerElementRef.current) {
            // if no change in query or type, re-run the search
            this.pagerElementRef.current.triggerSearch();
        }
        else {
            this.setState({query, type});
        }
    };

    /**
     * The Pager component deals with fetching the query from the server.
     * We use this callback to set the results on this component's state.
     *
     * @param {array} newResults - The array of results from the  query
     */
    searchResultsCallback = (newResults: any[]) => {
        this.setState({results: newResults});
    };

    /**
     * The Pager component is set up to react to browser history navigation (prev/next buttons),
     * and we use this callback to set the query and type on this component's state.
     *
     * @param {URLSearchParams} searchParams - The URL search parameters passed up from the pager component
     */
    searchParamsChangeCallback = (searchParams: URLSearchParams) => {
        let query;
        let type;
        if (searchParams.has('q')) {
            query = searchParams.get('q');
        }
        if (searchParams.has('type')) {
            type = searchParams.get('type');
        }
        if (query === this.state.query && type === this.state.type) {
            return;
        }
        this.handleSearch(query, type);
    };

    /**
     * Renders the component: Search bar with results table located vertically
     * below it.
     *
     * @returns {object} - JSX to render.
     */
    render() {
        const {type, query, results} = this.state;
        const querySearchParams = `q=${query}${type ? `&type=${type}` : ''}`;
        return (
            <div id="pageWithPagination">
                <SearchField
                    entityTypes={this.props.entityTypes}
                    query={query}
                    type={type}
                    onSearch={this.handleSearch}
                />
                <SearchResults
                    results={this.state.results}
                    user={this.props.user}
                />
                <PagerElement
                    from={this.props.from}
                    nextEnabled={this.props.nextEnabled}
                    paginationUrl={this.paginationUrl}
                    querySearchParams={querySearchParams}
                    ref={this.pagerElementRef}
                    results={results}
                    searchParamsChangeCallback={this.searchParamsChangeCallback}
                    searchResultsCallback={this.searchResultsCallback}
                    size={this.props.resultsPerPage}
                />
                <div className="text-center">
                    {results.length === 0 &&
                    <div>
                        <hr className="thin"/>
                        <h2 style={{color: '#754e37'}}>
                        No results found
                        </h2>
                    </div>}

                    <div>
                        {results.length === 0 &&
                            <small>Make sure the spelling is correct, and that
                                 you have selected the correct type in the search bar.
                            </small>}
                        <hr className="wide"/>
                        <h3>Are we missing an entry?</h3>
                        <CallToAction query={query}/>
                    </div>

                </div>
            </div>
        );
    }
}

export default SearchPage;