add search function - with Algolia
This commit is contained in:
parent
dcc9161c39
commit
e016235b8c
|
@ -10,6 +10,11 @@
|
||||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.search-highlight {
|
||||||
|
background-color: #FFFF00;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
/* html, body {
|
/* html, body {
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
unicode-range: U+0600-06FF;
|
unicode-range: U+0600-06FF;
|
||||||
|
|
22
src/App.tsx
22
src/App.tsx
|
@ -22,10 +22,7 @@ import { isProd } from "./lib/isProd";
|
||||||
import ReactGA from "react-ga";
|
import ReactGA from "react-ga";
|
||||||
import { useUser } from "./user-context";
|
import { useUser } from "./user-context";
|
||||||
import PrivacyPolicy from "./pages/PrivacyPolicy";
|
import PrivacyPolicy from "./pages/PrivacyPolicy";
|
||||||
// import algoliasearch from "algoliasearch";
|
import SearchPage from "./pages/SearchPage";
|
||||||
|
|
||||||
// const client = algoliasearch('M5GQZF38JA', '1e3b529b909acf72fde1515f520f3913');
|
|
||||||
// const index = client.initIndex('netlify_150beb8b-aae1-4cef-a05c-2add5d8904f7_master_all');
|
|
||||||
|
|
||||||
const chapters = content.reduce((chapters, item) => (
|
const chapters = content.reduce((chapters, item) => (
|
||||||
item.content
|
item.content
|
||||||
|
@ -64,33 +61,26 @@ function App() {
|
||||||
logAnalytics();
|
logAnalytics();
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
}, [window.location.pathname]);
|
}, [window.location.pathname]);
|
||||||
// function handleSearch(s: string) {
|
|
||||||
// setSearch(s);
|
|
||||||
// index.search(s, {
|
|
||||||
// attributesToSnippet: [
|
|
||||||
// "content:20",
|
|
||||||
// ],
|
|
||||||
// }).then(({ hits }) => {
|
|
||||||
// console.log(hits);
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Header setNavOpen={setNavOpen} />
|
<Header setNavOpen={setNavOpen} />
|
||||||
<div className="container-fluid">
|
<div className="container-fluid">
|
||||||
<div className="main-row row">
|
<div className="main-row row" style={{ minHeight: "calc(100vh - 62px)" }}>
|
||||||
<Sidebar
|
<Sidebar
|
||||||
content={content}
|
content={content}
|
||||||
navOpen={navOpen}
|
navOpen={navOpen}
|
||||||
setNavOpen={setNavOpen}
|
setNavOpen={setNavOpen}
|
||||||
pathname={window.location.pathname}
|
pathname={window.location.pathname}
|
||||||
/>
|
/>
|
||||||
{/* <input type="text" onChange={e => handleSearch(e.target.value)} value={search} /> */}
|
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route
|
<Route
|
||||||
path="/"
|
path="/"
|
||||||
element={<LandingPage />}
|
element={<LandingPage />}
|
||||||
/>
|
/>
|
||||||
|
<Route
|
||||||
|
path="/search"
|
||||||
|
element={<SearchPage />}
|
||||||
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="/privacy"
|
path="/privacy"
|
||||||
element={<PrivacyPolicy />}
|
element={<PrivacyPolicy />}
|
||||||
|
|
|
@ -28,6 +28,11 @@ function Header({ setNavOpen }) {
|
||||||
<h4 className="header-title link-unstyled mt-2"><Link to="/">Pashto Grammar</Link></h4>
|
<h4 className="header-title link-unstyled mt-2"><Link to="/">Pashto Grammar</Link></h4>
|
||||||
</div>
|
</div>
|
||||||
<div className="d-flex flex-row justify-content-right align-items-center">
|
<div className="d-flex flex-row justify-content-right align-items-center">
|
||||||
|
<div className="mr-4 link-unstyled">
|
||||||
|
<Link to="/search">
|
||||||
|
<i className={`fas fa-search fa-lg clickable`}></i>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
<div className="mr-3 link-unstyled">
|
<div className="mr-3 link-unstyled">
|
||||||
<Link to="/account">
|
<Link to="/account">
|
||||||
<i className={`fas ${user ? "fa-user" : "fa-sign-in-alt"} fa-lg clickable`}></i>
|
<i className={`fas ${user ? "fa-user" : "fa-sign-in-alt"} fa-lg clickable`}></i>
|
||||||
|
|
|
@ -0,0 +1,102 @@
|
||||||
|
import Link from "../components/Link";
|
||||||
|
import Footer from "../components/Footer";
|
||||||
|
import algoliasearch from "algoliasearch";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { createSearchParams, useSearchParams } from "react-router-dom";
|
||||||
|
|
||||||
|
const client = algoliasearch(
|
||||||
|
process.env.ALGOLIA_GRAMMAR_APP_ID || "",
|
||||||
|
process.env.ALGOLIA_GRAMMAR_API_KEY || "",
|
||||||
|
);
|
||||||
|
const index = client.initIndex(process.env.ALGOLIA_GRAMMAR_INDEX || "");
|
||||||
|
|
||||||
|
const SearchPage = () => {
|
||||||
|
const [search, setSearch] = useState("");
|
||||||
|
const [results, setResults] = useState<any[] | "searching" | "none">([]);
|
||||||
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
|
useEffect(() => {
|
||||||
|
const inParams = searchParams.get("search");
|
||||||
|
if (inParams) {
|
||||||
|
setSearch(inParams);
|
||||||
|
doSearch(inParams);
|
||||||
|
} else {
|
||||||
|
setResults([]);
|
||||||
|
setSearch("");
|
||||||
|
}
|
||||||
|
}, [window.location.search]);
|
||||||
|
function handleSearch(e: React.FormEvent<HTMLFormElement>) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!search) return;
|
||||||
|
setSearchParams(createSearchParams({
|
||||||
|
search,
|
||||||
|
}));
|
||||||
|
doSearch(search);
|
||||||
|
}
|
||||||
|
function doSearch(s: string) {
|
||||||
|
setResults("searching");
|
||||||
|
index.search(s, {
|
||||||
|
attributesToSnippet: [
|
||||||
|
"content:30",
|
||||||
|
],
|
||||||
|
highlightPreTag: '<strong class="search-highlight">',
|
||||||
|
highlightPostTag: '</strong>'
|
||||||
|
}).then(({ hits }) => {
|
||||||
|
setResults(hits.length ? hits : "none");
|
||||||
|
}).catch(e => {
|
||||||
|
console.error(e);
|
||||||
|
alert("Connect to the internet to search");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return <>
|
||||||
|
<main className="col bg-faded py-3 d-flex flex-column" style={{ maxWidth: "800px" }}>
|
||||||
|
<h1>Search</h1>
|
||||||
|
<form className="input-group mb-3 mt-2" onSubmit={handleSearch}>
|
||||||
|
<input
|
||||||
|
className="form-control"
|
||||||
|
placeholder="Search in grammar..."
|
||||||
|
type="text"
|
||||||
|
onChange={e => setSearch(e.target.value)}
|
||||||
|
value={search}
|
||||||
|
/>
|
||||||
|
<div className="input-group-append">
|
||||||
|
<button className="btn btn-outline-primary" type="submit">
|
||||||
|
Search
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{results === "none"
|
||||||
|
? <h5>No results found</h5>
|
||||||
|
: results === "searching"
|
||||||
|
? <p>Searching...</p>
|
||||||
|
: <>
|
||||||
|
{results.length > 0 && <div className="text-muted small mb-3">{`${results.length} result${results.length > 1 ? "s" : ""}`}</div>}
|
||||||
|
{results.map(result => <div className="link-unstyled mb-4">
|
||||||
|
<Link to={result.url}>
|
||||||
|
<div>
|
||||||
|
<h5>{getHiearchy(result.hierarchy)}</h5>
|
||||||
|
<div
|
||||||
|
dangerouslySetInnerHTML={{__html: result._snippetResult.content.value }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
</div>)}
|
||||||
|
</>}
|
||||||
|
{/*
|
||||||
|
@ts-ignore */}
|
||||||
|
<Footer />
|
||||||
|
</main>
|
||||||
|
</>;
|
||||||
|
};
|
||||||
|
|
||||||
|
function getHiearchy(s: any): string {
|
||||||
|
const levels: string[] = [s.lvl0];
|
||||||
|
for (let i = 1; i < 6; i++) {
|
||||||
|
const key = `lvl${i}`;
|
||||||
|
if (s[key]) {
|
||||||
|
levels.push(s[key]);
|
||||||
|
} else break;
|
||||||
|
}
|
||||||
|
return levels.join(" • ");
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SearchPage;
|
Loading…
Reference in New Issue