HomeDev guideRecipesAPI Reference
Dev guideUser GuidesLegal TermsNuGetDev CommunityOptimizely AcademySubmit a ticketLog In
Dev guide

Create a search page using Optimizely Graph

Prerequisites

Tutorial

  1. Go to the components folder with the cd src/components command.

  2. Create a new component with touch SearchButton.tsx command.

    import { useState } from "react";
    import { useSearchParams } from "react-router-dom";
    
    type CustomString = string | number | readonly string[] | undefined
    
    function SearchButton(): JSX.Element {
        const [searchParams] = useSearchParams()
        const [searchValue, setSearchValue] = useState<CustomString>(searchParams.get("q")?.toString())
        
        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);
        }
    
        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>
            </div>
        );
    }
    
    export default SearchButton;
    
  3. Go to the graphql folder using the cd/graphql command.

  4. Create two new GraphQL files using touch commands:
    ArtistSearch.graphql

    query ArtistSearch($searchParam: String, $locales: Locales!, $order: OrderBy) {
      ArtistDetailsPage(
        locale: [$locales]
        orderBy: { _ranking: RELEVANCE, ArtistName: $order }
        where: { _or: [{ Name: { contains: $searchParam, boost: 10 } }, { Name: { startsWith: $searchParam, boost: 10 } }, { StageName: { startsWith: $searchParam } }] }
      ) {
        items {
          PerformanceStartTime
          PerformanceEndTime
          StageName
          ArtistName
          ArtistPhoto
          ArtistGenre
          ArtistDescription
          ArtistIsHeadliner
          RelativePath
          ParentLink {
            Url
          }
          _fulltext
        }
      }
    }
    

    OtherContentSearch.graphql

    query OtherContentSearch($searchParam: String, $locales: Locales!, $order: OrderBy) {
      Content(
        locale: [$locales]
        orderBy: { _ranking: RELEVANCE, Name: $order }
        where: { _or: [
          { Name: { contains: $searchParam, boost: 10 } }, 
          { Name: { startsWith: $searchParam, boost: 10 } }
        ],
          _and:{
            ContentType:{
              notEq: "ArtistDetailsPage"
            }
          }
        }
      ) {
        items {
          Name
          RelativePath
          ParentLink {
            Url
          }
          _fulltext
          ContentType
        }
      }
    }
    
  5. Go to the pages folder using the cd src/pages command.

  6. Create a new page using the touch SearchPage.tsx command.
    SearchPage.tsx

    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 { ArtistSearchQuery, OtherContentSearchQuery, 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 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 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);
      }
    
      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-panel">
              <div style={{float: "left"}}>
                <SearchButton />
              </div>
              <div style={{float: "right"}}>
                <span>Filter 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-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-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>
                            &nbsp;
                            <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>
            <Footer content={null}/>
          </div>
        </div>
      );
    }
    
    export default SearchPage;
    
  7. Modify LandingPage.tsx page to create a new search box for the music festival site.
    LandingPage.tsx

    <div className="nav-table-row">
      <div className="nav-table-cell">
        <button className="Button buy-ticket-button">{content?.BuyTicketBlock?.Heading}</button>
      </div>
      <div className="nav-table-cell search-button-block">
        <SearchButton />
      </div>
    </div>
    
  8. Start the site with yarn start and you can see the search box now. It will redirect to the search page, which contains the search result.