<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/[email protected]^17.0.2";
import ReactDOM from "https://cdn.skypack.dev/[email protected]^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
This Pen doesn't use any external CSS resources.
This Pen doesn't use any external JavaScript resources.