Apply autocomplete to the search page
This tutorial applies autocomplete to the search page for the Music Festival site.
Prerequisite
- Create a React site using data from Optimizely Graph.
- Optimizely Content Delivery - Decoupled.
- Create a search page using Optimizely Graph.
Tutorial
-
Go to
/decoupled-site/react-script/src/graphql
. -
Add a new file
ArtistAutocomplete.graphql
, and add the following code.query ArtistAutocomplete($searchParam: String!) { ArtistDetailsPage { autocomplete { StageName(value: $searchParam) ArtistName(value: $searchParam, limit: 3) } } }
-
Go to the
components
folder, openSearchButton.tsx
and update the file as shown in the following code block.import { Autocomplete, TextField } from "@mui/material"; import { useState } from "react"; import { useSearchParams } from "react-router-dom"; import { ArtistAutocompleteQuery, useArtistAutocompleteQuery } from "../generated"; import { generateGQLSearchQueryVars } from "../helpers/queryCacheHelper"; import { isEditOrPreviewMode } from "../helpers/urlHelper"; const singleKeyUrl = process.env.REACT_APP_CONTENT_GRAPH_GATEWAY_URL as string type CustomString = string | number | readonly string\[] | undefined function SearchButton(): JSX.Element { const [searchParams] = useSearchParams() const [token, setToken] = useState("") const [isShown, setIsShown] = useState(false) const [searchValue, setSearchValue] = useState<CustomString>(searchParams.get("q")?.toString()) const [orderBy, setOrderBy] = useState("ASC") let variables: any = generateGQLSearchQueryVars(token, window.location.pathname, searchValue as string | null, orderBy); const modeEdit = isEditOrPreviewMode() let stringArr: (string | null)\[] = \[] let autocompleteData : ArtistAutocompleteQuery | undefined = undefined const { data : artistAutocompleteData } = useArtistAutocompleteQuery({ endpoint: singleKeyUrl }, variables, { staleTime: 2000, enabled: !modeEdit || !!token }) autocompleteData = artistAutocompleteData function search(event: any, action: string){ if ((action == "keypress" && event.charCode === 13) || action == "buttonclick") { window.location.href = `${window.location.origin}/search?q=${searchValue}` } } function onValueChange(event: any){ setSearchValue(event.target.value); event.target.value != "" && event.target.value !== undefined ? setIsShown(true) : setIsShown(false); } function onAutoClick(event: any){ setSearchValue(event.target.textContent); window.location.href = `${window.location.origin}/search?q=${event.target.textContent}` } return ( <div> <div className="nav-table-cell"> <input className="search-input" type="text" id="search-input" placeholder="Search" onKeyPress={(event) => {search(event, 'keypress')}} value={searchValue} onChange={onValueChange} /> <a className="search-icon" onClick={(event) => {search(event, 'buttonclick')}}> <i className="fa fa-search"></i> </a> <div className="autocomplete-block" style={isShown ? {display: "inherit"}: {display: "none"}}> { autocompleteData?.ArtistDetailsPage?.autocomplete?.ArtistName?.map((name) => { return( <div key={name} onClick={(event) => onAutoClick(event)}>{name}</div> ) }) } { autocompleteData?.ArtistDetailsPage?.autocomplete?.StageName?.map((name) => { return( <div key={name} onClick={(event) => onAutoClick(event)}>{name}</div> ) }) } </div> </div> </div> ); } export default SearchButton;
-
Go to the
SearchPage.tsx
and update the file with the following code.import { useState } from "react"; import { useSearchParams } from "react-router-dom"; import Footer from "../components/Footer"; import Header from "../components/Header"; import SearchButton from "../components/SearchButton"; import { ArtistAutocompleteQuery, ArtistSearchQuery, OtherContentSearchQuery, useArtistAutocompleteQuery, useArtistSearchQuery, useOtherContentSearchQuery } from "../generated"; import { generateGQLSearchQueryVars } from "../helpers/queryCacheHelper"; import { getImageUrl, isEditOrPreviewMode } from "../helpers/urlHelper"; import ReactPaginate from 'react-paginate'; const singleKeyUrl = process.env.REACT_APP_CONTENT_GRAPH_GATEWAY_URL as string function SearchPage() { const [token, setToken] = useState("") const [itemOffset, setItemOffset] = useState(0) const [otherItemOffset, setOtherItemOffset] = useState(0) const [itemsPerPage, setItemsPerPage] = useState(10) const [otherItemsPerPage, setOtherItemsPerPage] = useState(10) const [filterBy, setFilterBy] = useState("Artist") const [orderBy, setOrderBy] = useState("ASC") const [searchParams] = useSearchParams() const endOffset = itemOffset + itemsPerPage; const endOffsetOther = otherItemOffset + otherItemsPerPage; const modeEdit = isEditOrPreviewMode() let data: ArtistSearchQuery | undefined = undefined let otherData: OtherContentSearchQuery | undefined = undefined let autocompleteData : ArtistAutocompleteQuery | undefined = undefined let queryString: string | null let resultNumber : number let otherResultNumber : number let variables: any let options: {value: string; key: string}[] = [ {value: "ASC", key: "ASC"}, {value: "DESC", key: "DESC"} ] let itemsPerPageOptions: {value: number; key: string}[] = [ {value: 10, key: "10"}, {value: 15, key: "15"} ] let filterByOption: {value: string; key: string}[] = [ {value: "Artists", key: "Artist"}, {value: "Other Content", key: "OtherContent"} ] queryString = searchParams.get("q") if(queryString === undefined || queryString == 'undefined'){ queryString = "" } variables = generateGQLSearchQueryVars(token, window.location.pathname, queryString, orderBy); const { data : searchQueryData } = useArtistSearchQuery({ endpoint: singleKeyUrl }, variables, { staleTime: 2000, enabled: !modeEdit || !!token }); data = searchQueryData resultNumber = data?.ArtistDetailsPage?.items?.length ?? 0 const currentItems = data?.ArtistDetailsPage?.items?.slice(itemOffset, endOffset); const pageCount = Math.ceil(resultNumber / itemsPerPage); const { data : otherContentSearchQueryData } = useOtherContentSearchQuery({ endpoint: singleKeyUrl }, variables, { staleTime: 2000, enabled: !modeEdit || !!token }); otherData = otherContentSearchQueryData otherResultNumber = otherData?.Content?.items?.length ?? 0 const currentOtherItems = otherData?.Content?.items?.slice(otherItemOffset, endOffsetOther); const pageOtherCount = Math.ceil(otherResultNumber / itemsPerPage); const { data : artistAutocompleteData } = useArtistAutocompleteQuery({ endpoint: singleKeyUrl }, variables, { staleTime: 2000, enabled: !modeEdit || !!token }) autocompleteData = artistAutocompleteData const handlePageClick = (event: any) => { const newOffset = (event.selected * itemsPerPage) % resultNumber; setItemOffset(newOffset); }; const handleItemsChange = (event: any) => { setItemsPerPage(event.target.value); }; const handleOtherPageClick = (event: any) => { const newOffset = (event.selected * itemsPerPage) % otherResultNumber; setOtherItemOffset(newOffset); }; const handleOtherItemsChange = (event: any) => { setOtherItemsPerPage(event.target.value); }; const handleFilterByChange = (event: any) => { setFilterBy(event.target.value); }; const handleChange = (event: any) => { setOrderBy(event.target.value); } const handleFacetClick = (event: any) => { window.location.href = `${window.location.origin}/search?q=${event.target.innerText}` } return ( <div> <Header /> <div className="search-container"> <div className="back-button"> <a href={window.location.origin} className="home-link"> <span>Back to Landing page</span> </a> </div> <div className="search-zone"> <div style={{float: "left"}}> <SearchButton /> </div> <div style={{float: "right"}}> <span>Search by: </span> <select className="Button" onChange={handleFilterByChange}> { filterByOption.map((option) => { return ( <option key={option.key} value={option.key}>{option.value}</option> ) }) } </select> </div> </div> <div className="search-panel"> <div className="left-panel"> <b>Filter by: </b> <div className="facets" style={filterBy == "Artist" ? {display: "inherit"}: {display: "none"}}> <b>Artist Name: </b> { data?.ArtistDetailsPage?.facets?.ArtistName?.map((artist) => { return ( <div> <a key={artist?.name} onClick={(event) => handleFacetClick(event)}> <span>{artist?.name}</span> <b>({artist?.count})</b> </a> </div> ) }) } </div> <div className="facets" style={filterBy == "Artist" ? {display: "inherit"}: {display: "none"}}> <b>Stage Name: </b> { data?.ArtistDetailsPage?.facets?.StageName?.map((artist) => { return ( <div> <a key={artist?.name} onClick={(event) => handleFacetClick(event)}> <span>{artist?.name}</span> <b>({artist?.count})</b> </a> </div> ) }) } </div> <div className="facets" style={filterBy == "OtherContent" ? {display: "inherit"}: {display: "none"}}> <b>Content: </b> { otherData?.Content?.facets?.Name?.map((content) => { return ( <div> <a key={content?.name} onClick={(event) => handleFacetClick(event)}> <span>{content?.name}</span> <b>({content?.count})</b> </a> </div> ) }) } </div> </div> <div className="right-panel"> <div className="search-description"> <h6>Your search for <span className="search-term">{queryString}</span> resulted in <span className="search-term">{filterBy == "Artist" ? resultNumber : otherResultNumber}</span> hits</h6> </div> <div className="search-sorting"> <span>Sort: </span> <select onChange={e => handleChange(e)} className="Button"> { options.map((option) => { return ( <option key={option.key} value={option.key}>{option.value}</option> ) }) } </select> </div> <div className="result-block"> <div style={filterBy == "Artist" ? {display: "initial"}: {display: "none"}}> <div className="search-results"> { currentItems?.map((content, idx) => { return ( <div className="result" key={idx}> <div className="card"> <div className="round"> <img className="ConditionalImage" src={getImageUrl(content?.ArtistPhoto ?? '')} alt={content?.ArtistName ?? ''} /> </div> <div className="info"> <a href={content?.RelativePath ?? ''} className="EPiLink"> <p className="result-name">{content?.ArtistName}</p> </a> </div> </div> <div> <p className="result-description">{content?.ArtistDescription}</p> </div> </div> ) }) } </div> <div className="search-description"> <h6>People also search for: </h6> <br></br> { autocompleteData?.ArtistDetailsPage?.autocomplete?.ArtistName?.map((name) => { return ( <div> <a key={name} onClick={(event) => handleFacetClick(event)}> <i>{name}</i> </a> </div> ) }) } { autocompleteData?.ArtistDetailsPage?.autocomplete?.StageName?.map((name) => { return ( <div> <a key={name} onClick={(event) => handleFacetClick(event)}> <i>{name}</i> </a> </div> ) }) } </div> <div className="search-pagination-block"> <table> <tbody> <tr> <td> <span>Items per page: </span> <select className="Button" onChange={handleItemsChange}> { itemsPerPageOptions.map((option) => { return ( <option key={option.key} value={option.key}>{option.value}</option> ) }) } </select> </td> <td className="search-pagination"> <ReactPaginate breakLabel="..." nextLabel=">" onPageChange={handlePageClick} pageRangeDisplayed={5} pageCount={pageCount} previousLabel="<" renderOnZeroPageCount={null} /> </td> </tr> </tbody> </table> </div> </div> <div style={filterBy == "OtherContent" ? {display: "initial"}: {display: "none"}}> <div className="search-results"> { currentOtherItems?.map((content, idx) => { return ( <div className="result" key={idx}> <div className="card"> <i className="fa fa-file"></i> <div className="info"> <a href={content?.RelativePath ?? ''} className="EPiLink"> <p className="result-name">{content?.Name}</p> </a> </div> </div> <div> <p className="result-description">{content?.RelativePath}</p> </div> </div> ) }) } </div> <div className="search-pagination-block"> <table> <tbody> <tr> <td> <span>Items per page: </span> <select className="Button" onChange={handleOtherItemsChange}> { itemsPerPageOptions.map((option) => { return ( <option key={option.key} value={option.key}>{option.value}</option> ) }) } </select> </td> <td className="search-pagination"> <ReactPaginate breakLabel="..." nextLabel=">" onPageChange={handleOtherPageClick} pageRangeDisplayed={5} pageCount={pageOtherCount} previousLabel="<" renderOnZeroPageCount={null} /> </td> </tr> </tbody> </table> </div> </div> </div> </div> </div> <Footer content={null}/> </div> </div> ); } export default SearchPage;
-
Start the site. From now on, whenever you search for a phrase, the system automatically displays results that match with your search phrase. Also, at the end of the search page, the People also search for section displays results that match your search term.
Updated 4 months ago