Skip to content

Commit

Permalink
feat: implement search system (TuanManhCao#10)
Browse files Browse the repository at this point in the history
* chore: add wanakana and fuse.js

I had inadvertently committed fuze.js at TuanManhCao#9...

* feat: add SearchBox component

* feat: add SearchData interface

* feat: export getRouterPath function

* style: suppress error

* feat: add search index creation process

* style: fix result text align

* feat: add search element

* fix: result click behavior

This problem occurred by the parent onBlur event handled before the result onClick event ran.

* style: format code

* fix: set result length limit up to 5

* chore: ignore search-index.json

* chore: add tailwind config files

I forgot adding these file in git at TuanManhCao#9

* chore: add ts-pattern

* feat: add target text display system
  • Loading branch information
turtton committed Feb 9, 2023
1 parent 7474526 commit c973e0b
Show file tree
Hide file tree
Showing 13 changed files with 310 additions and 18 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,5 @@ yarn-error.log*

.vercel

/graph-data.json
/graph-data.json
/search-index.json
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"@mui/icons-material": "latest",
"@mui/lab": "latest",
"@mui/material": "latest",
"@types/wanakana": "^4.0.3",
"cytoscape-d3-force": "^1.1.4",
"cytoscape-node-html-label": "^1.2.1",
"d3": "^6.2.0",
Expand Down Expand Up @@ -48,9 +49,11 @@
"remark-rehype": "^10.1.0",
"remark-wiki-link": "^1.0.0",
"to-vfile": "^6.1.0",
"ts-pattern": "^4.1.4",
"unified": "^9.2.0",
"unist-util-visit": "^4.1.0",
"vfile-reporter": "^6.0.1"
"vfile-reporter": "^6.0.1",
"wanakana": "^5.0.2"
},
"devDependencies": {
"@babel/core": "^7.20.12",
Expand Down
19 changes: 19 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions postcss.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
11 changes: 8 additions & 3 deletions src/components/RootContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import FolderTree from "./FolderTree";
import MDContent from "./MDContentData";
import { Prop } from "../pages";
import dynamic from "next/dynamic";
import { SearchBar } from "./Search";

interface HomeElement extends HTMLElement {
checked: boolean;
Expand All @@ -19,6 +20,7 @@ export default function RootContainer({
tree,
flattenNodes,
backLinks,
searchIndex,
}: Prop): JSX.Element {
const burgerId = "hamburger-input";
const closeBurger = (): void => {
Expand All @@ -42,9 +44,12 @@ export default function RootContainer({
<DynamicGraph graph={graphData} />
</nav>
</div>
<nav className="nav-bar">
<FolderTree tree={tree} flattenNodes={flattenNodes} />
</nav>
<div>
<nav className="nav-bar">
<SearchBar index={searchIndex} />
<FolderTree tree={tree} flattenNodes={flattenNodes} />
</nav>
</div>
<MDContent content={content} backLinks={backLinks} />
<DynamicGraph graph={graphData} />
</div>
Expand Down
157 changes: 157 additions & 0 deletions src/components/Search.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import { TextField } from "@mui/material";
import Fuse from "fuse.js";
import IFuseOptions = Fuse.IFuseOptions;
import { SearchData } from "../lib/search";
import { Dispatch, SetStateAction, useState } from "react";
import { useRouter } from "next/router";
import { isJapanese, toRomaji } from "wanakana";
import { match } from "ts-pattern";

interface SearchProp {
index: readonly SearchData[];
}

interface ResultProp {
contents: DataResult;
hidden: boolean;
setHidden: Dispatch<SetStateAction<boolean>>;
setMouseOvering: Dispatch<SetStateAction<boolean>>;
}

interface SearchResult extends SearchData {
startPos: number;
key: string | undefined;
}

type DataResult = readonly SearchResult[] | "Empty";

const fuseConfig: IFuseOptions<SearchData> = {
keys: ["title", "rawTitle", "singleLineContent", "rawContent.raw"],
includeMatches: true,
};

export function SearchBar(prop: SearchProp): JSX.Element {
const fuse = new Fuse(prop.index, fuseConfig);
const [hitData, setHitData] = useState("Empty" as DataResult);
const [isResultHidden, setResultHidden] = useState(false);
const [isMouseOveringResult, setMouseOveringResult] = useState(false);
return (
<div
className="mb-4 px-2.5"
onBlur={() => {
if (!isMouseOveringResult) {
setResultHidden(true);
}
}}
>
<TextField
className="bg-white"
fullWidth
label="Search"
onFocus={() => {
setResultHidden(false);
}}
onChange={(input) => {
let text = input.target.value;
if (text.length === 0) {
setHitData("Empty");
return;
}
if (isJapanese(text)) {
text = toRomaji(text);
}
let result = fuse.search(text).map((r) => {
const item = r.item as SearchResult;
const matches = r.matches;
if (matches === undefined || matches.length < 1) return item;
item.startPos = matches[0].indices[0][0] ?? 0;
item.key = matches[0].key ?? "";
return item;
});
const titles = result.map((r) => r.title);
result = result.filter((value, index) => index === titles.indexOf(value.title));
if (result.length > 5) {
result.length = 5;
}
setHitData(result);
}}
/>
<ResultList
contents={hitData}
hidden={isResultHidden}
setHidden={setResultHidden}
setMouseOvering={setMouseOveringResult}
/>
</div>
);
}

function ResultList({ contents, hidden, setHidden, setMouseOvering }: ResultProp): JSX.Element {
const router = useRouter();
if (contents === "Empty") {
return <div hidden={true} />;
}
return (
<div
className="absolute z-10 mt-2 w-11/12 divide-y divide-gray-100 rounded-lg bg-white shadow dark:bg-gray-700"
hidden={hidden}
>
<ul className="py-2 text-sm text-gray-700 dark:text-gray-200">
{contents.length === 0 ? (
<li className="inline-flex w-full px-4 py-2">
<p>Not Found</p>
</li>
) : (
contents.map((data) => (
<li
key={data.rawTitle}
className="inline-flex w-full px-4 py-2 dark:hover:bg-gray-600 dark:hover:text-white"
onClick={(): void => {
// TODO: Route target text line
void router.push(data.path);
setHidden(true);
}}
onMouseOver={() => {
setMouseOvering(true);
}}
onMouseLeave={() => {
setMouseOvering(false);
}}
>
<button className="w-full truncate" key={data.title}>
<p className="w-full truncate text-left underline">{data.title}</p>
<p className="truncate text-left text-xs text-gray-500">
{data.lineAt}:{" "}
{match<string | undefined, string>(data.key)
.with("singleLineContent", (): string =>
data.singleLineContent.substring(data.startPos),
)
.with("rawContent.raw", (): string => {
let startDistance = data.startPos;
const rawContent = data.rawContent;
if (rawContent === null) return data.singleLineContent;
let result: string | null = null;
rawContent.separatedRaw.forEach((value, index) => {
if (startDistance <= value.length) {
result = rawContent.separatedOriginal
.filter((value, originalIndex) => index <= originalIndex)
.join("");
if (index !== 0) {
result = `...${result}`;
}
return;
}
startDistance = startDistance - value.length;
});
return result ?? data.singleLineContent;
})
.otherwise(() => data.singleLineContent)}
</p>
</button>
</li>
))
)}
</ul>
</div>
);
}
14 changes: 2 additions & 12 deletions src/lib/markdown.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { DirectoryTree } from "directory-tree";
import { getAllSlugs, toFilePath } from "./slug";
import { Transformer } from "./transformer";
import { getRouterPath } from "./slug";

export interface MdObject {
name: string;
Expand All @@ -13,16 +12,7 @@ export function convertObject(thisObject: DirectoryTree): MdObject {
const children: MdObject[] = [];

const objectName = thisObject.name;
let routerPath: string | null =
getAllSlugs().find((slug) => {
const fileName = Transformer.parseFileNameFromPath(toFilePath(slug));
return (
Transformer.normalizeFileName(fileName ?? "") === Transformer.normalizeFileName(objectName)
);
}) ?? "";

const nameAndExtension = objectName.split(".");
routerPath = nameAndExtension.length > 1 && routerPath !== "" ? "/note/" + routerPath : null;
const routerPath = getRouterPath(objectName);
const newObject: MdObject = {
name: objectName,
children,
Expand Down

0 comments on commit c973e0b

Please sign in to comment.