<div id="root"></div>
body {
  line-height: 1.5;
}
* {
  margin-top: 0;
}
.container {
  padding: 1rem 0.5rem;
  max-width: 75ch;
  margin: auto;
}
label {
  display: block;
}
h1,
h2 {
  margin-bottom: 0;
}

h2 {
  font-size: 1.25rem;
}
h1 + p {
  color: teal;
}
input[type="search"] {
  margin-bottom: 1rem;
  padding: 0.5rem;
  border: 1px solid;
}
li {
  margin-bottom: 0.5rem;
}
.sort {
  display: flex;
  gap: 1rem;
}
import React from "https://cdn.skypack.dev/react@^17.0.2";
import ReactDOM from "https://cdn.skypack.dev/react-dom@^17.0.2";
import AwesomeDebouncePromise from "https://cdn.skypack.dev/awesome-debounce-promise";
import parse from "https://cdn.skypack.dev/html-react-parser";
const { useEffect, useReducer, useRef } = React;

const serviceUrl =
  "https://public-api.wordpress.com/rest/v1.3/sites/45537868/search?filter[bool][must][0][term][post_type]=post&size=10&highlight_fields[0]=title&highlight_fields[1]=content";

const TIME_INTERVAL = 300; // ms

const searchFunction = async (apiUrl, { searchTerm, sort, pageHandle }) => {
  const url = new URL(apiUrl);
  url.searchParams.set("query", searchTerm);
  url.searchParams.set("sort", sort);
  if (pageHandle) {
    url.searchParams.set("page_handle", pageHandle);
  }
  const result = await fetch(url);
  if (result.status !== 200) {
    throw new Error("bad status = " + result.status);
  }
  const json = await result.json();
  return { ...json, isPaginatedResult: !!pageHandle };
};

const reducer = (state, action) => {
  switch (action.type) {
    case "FETCH_INIT":
      return {
        ...state,
        loading: true
      };
    case "FETCH_SUCCESS_SIMPLE":
      return {
        ...state,
        loading: false,
        error: null,
        data: action.payload
      };
    case "FETCH_SUCCESS_MORE":
      return {
        ...state,
        loading: false,
        error: null,
        data: {
          ...action.payload,
          results: [...state.data.results, ...action.payload.results]
        }
      };
    case "ERROR":
      return {
        data: null,
        loading: false,
        error: action.payload
      };
    default:
      throw new Error("unsupporter action type");
  }
};

const paramsReducer = (state, action) => {
  return {
    ...state,
    ...action,
    pageHandle: action.pageHandle || ""
  };
};

// Generic reusable hook
const useDebouncedInstantSearch = (params) => {
  const [state, dispatch] = useReducer(reducer, {
    loading: false,
    error: null,
    data: {}
  });

  const debouncedSearchFunctionRef = useRef(
    AwesomeDebouncePromise(searchFunction, TIME_INTERVAL)
  );

  useEffect(() => {
    dispatch({ type: "FETCH_INIT" });
    const runAsyncSearch = async () => {
      if (!params.searchTerm) {
        return { results: [] };
      }
      if (params.pageHandle) {
        // we don't need to debounce the load more request
        return searchFunction(serviceUrl, params);
      } else {
        return debouncedSearchFunctionRef.current(serviceUrl, params);
      }
    };
    runAsyncSearch()
      .then((result) => {
        if (result.isPaginatedResult) {
          return dispatch({
            type: "FETCH_SUCCESS_MORE",
            payload: result
          });
        }
        return dispatch({
          type: "FETCH_SUCCESS_SIMPLE",
          payload: result
        });
      })
      .catch((error) => {
        return dispatch({ type: "ERROR", payload: error.message });
      });
  }, [params]);

  return state;
};

const SearchResults = ({ params, setParams }) => {
  const { loading, error, data } = useDebouncedInstantSearch(params);
  const { searchTerm } = params;
  if (error) {
    return <p>Error - {error}</p>;
  }
  return (
    <section className="search-results">
      {loading ? (
        <p className="info">Searching posts .....</p>
      ) : (
        <>
          {data.total !== undefined && (
            <p>
              Found {data.total} results for{" "}
              {data.corrected_query ? (
                <>
                  <del>{searchTerm}</del> <span>{data.corrected_query}</span>
                </>
              ) : (
                <span>{searchTerm}</span>
              )}
            </p>
          )}
        </>
      )}
      {data.results?.length > 0 && (
        <ul>
          {data.results.map((el) => {
            return (
              <li key={el.fields.post_id}>
                <h2>
                  {el.highlight.title[0]
                    ? el.highlight.title.map((item, index) => (
                        <React.Fragment key={index}>
                          {parse(item)}
                        </React.Fragment>
                      ))
                    : parse(el.fields["title.default"])}
                </h2>

                <div className="post-excerpt">
                  {el.highlight.content[0]
                    ? el.highlight.content.map((item, index) => (
                        <div key={index}>{parse(item)}</div>
                      ))
                    : parse(el.fields["excerpt.default"])}
                </div>
              </li>
            );
          })}
        </ul>
      )}
      {data.page_handle && (
        <button
          type="button"
          disabled={loading}
          onClick={() =>
            setParams({
              pageHandle: data.page_handle
            })
          }
        >
          {loading ? "loading..." : "load more"}
        </button>
      )}
    </section>
  );
};

const SearchForm = ({ params, setParams }) => {
  const sortChoices = [
    { key: "score_default", label: "Relevance" },
    { key: "date_asc", label: "Newest" },
    { key: "date_desc", label: "Oldest" }
  ];
  return (
    <>
      <label htmlFor="search">Search for posts</label>
      <input
        id="search"
        type="search"
        placeholder="ex. search"
        value={params.searchTerm}
        onChange={(e) => setParams({ searchTerm: e.target.value })}
        autoComplete="off"
      />
      <div className="sort">
        {sortChoices.map(({ key, label }) => {
          return (
            <label key={key}>
              <input
                type="radio"
                value={key}
                checked={params.sort === key}
                onChange={() => setParams({ sort: key })}
                name="sort"
              />{" "}
              {label}
            </label>
          );
        })}
      </div>
    </>
  );
};

const App = () => {
  const [params, setParams] = useReducer(paramsReducer, {
    searchTerm: "",
    sort: "score_default",
    pageHandle: ""
  });

  return (
    <section className="container">
      <h1>Instant search CSS-Tricks with Jetpack </h1>
      <p>
        <b>Posts</b> results only
      </p>
      <SearchForm params={params} setParams={setParams} />
      <SearchResults params={params} setParams={setParams} />
    </section>
  );
};
ReactDOM.render(<App />, document.getElementById("root"));
View Compiled

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.