- implemented basic search feature

- added tag list comonent to replace tables
This commit is contained in:
Kim, Jimin 2021-08-03 12:49:33 +09:00
parent 06ade73ac1
commit f731826368
10 changed files with 143 additions and 55 deletions

1
.gitignore vendored
View file

@ -3,6 +3,7 @@ _/
# auto generated files
source/src/data/map.json
source/src/data/search.json
source/src/data/content/
# dependencies

View file

@ -15,6 +15,7 @@ Tools / Frameworks / Packages used:
| [gray-matter](https://github.com/jonschlinkert/gray-matter) | Parsing markdown files |
| [react-tooltip](https://github.com/wwayne/react-tooltip) | Tooltips |
| [react-date-range](https://github.com/hypeserver/react-date-range) | Date picker for search page |
| [elasticlunr](https://github.com/weixsong/elasticlunr.js) | Search engine |
# Setup

View file

@ -8,6 +8,7 @@
import fs from "fs" // read and write files
import path from "path" // get relative path
import elasticlunr from "elasticlunr" // search index generation
import matter from "gray-matter" // parse markdown metadata
import toc from "markdown-toc" // table of contents generation
@ -68,6 +69,14 @@ interface Map {
}
}
interface SeriesMap {
// key: url
[key: string]: {
index: number
url: string
}[]
}
// searchable data that will be converted to JSON string
const map: Map = {
date: {},
@ -79,16 +88,12 @@ const map: Map = {
series: {},
unsearchable: {},
}
interface SeriesMap {
// key: url
[key: string]: {
index: number
url: string
}[]
}
const seriesMap: SeriesMap = {}
const index = elasticlunr(function () {
this.addField("title" as never)
this.addField("body" as never)
this.setRef("url" as never)
})
// converts file path to url
function path2URL(pathToConvert: string): string {
@ -209,6 +214,11 @@ function recursiveParsePosts(fileOrFolderPath: string) {
}
map.posts[urlPath] = postData
index.addDoc({
title: parsedMarkdown.data.title,
body: parsedMarkdown.content,
url: urlPath,
})
}
}
@ -244,7 +254,12 @@ function recursiveParseUnsearchable(fileOrFolderPath: string) {
return
}
const urlPath = path2URL(fileOrFolderPath)
const _urlPath = path2URL(fileOrFolderPath)
const urlPath = _urlPath.slice(
_urlPath
.slice(1) // ignore the first slash
.indexOf("/") + 1
)
// parse markdown metadata
const parsedMarkdown = matter(fs.readFileSync(fileOrFolderPath, "utf8"))
@ -254,7 +269,7 @@ function recursiveParseUnsearchable(fileOrFolderPath: string) {
}
// urlPath starts with a slash
const contentFilePath = `${contentDirectoryPath}${urlPath}.json`
const contentFilePath = `${contentDirectoryPath}/unsearchable${urlPath}.json`
// create directory to put json content files
fs.mkdirSync(
@ -271,15 +286,15 @@ function recursiveParseUnsearchable(fileOrFolderPath: string) {
)
// Parse data that will be written to map.js
map.unsearchable[
urlPath.slice(
urlPath
.slice(1) // ignore the first slash
.indexOf("/") + 1
)
] = {
map.unsearchable[urlPath] = {
title: parsedMarkdown.data.title,
}
index.addDoc({
title: parsedMarkdown.data.title,
body: parsedMarkdown.content,
url: urlPath,
})
}
}
@ -395,6 +410,11 @@ function recursiveParseSeries(fileOrFolderPath: string) {
map.series[urlPath] = { ...postData, order: [], length: 0 }
} else {
map.posts[urlPath] = postData
index.addDoc({
title: parsedMarkdown.data.title,
body: parsedMarkdown.content,
url: urlPath,
})
for (const key of Object.keys(map.series)) {
if (urlPath.slice(0, urlPath.lastIndexOf("/")).includes(key)) {
const index = parseInt(
@ -494,5 +514,5 @@ for (const seriesURL in seriesMap) {
map.series[seriesURL].order = seriesMap[seriesURL].map((item) => item.url)
}
// write to src/data/map.json
fs.writeFileSync(mapFilePath, JSON.stringify(map))
fs.writeFileSync(outPath + "/search.json", JSON.stringify(index))

View file

@ -21,7 +21,9 @@
"@fortawesome/free-regular-svg-icons": "^5.15.3",
"@fortawesome/free-solid-svg-icons": "^5.15.3",
"@fortawesome/react-fontawesome": "^0.1.14",
"@types/elasticlunr": "^0.9.2",
"date-fns": "^2.23.0",
"elasticlunr": "^0.9.5",
"gray-matter": "^4.0.3",
"local-storage-fallback": "^4.1.2",
"markdown-toc": "^1.2.0",

View file

@ -4,7 +4,9 @@ import styled from "styled-components"
import { Link } from "react-router-dom"
import theming from "../theming"
import Tag from "../components/Tag"
import TagList from "../components/TagList"
const StyledTitle = styled.h1`
font-size: 2rem;
@ -68,19 +70,20 @@ export default class PostCard extends React.Component<PostCardProps> {
</StyledLink>
</StyledTitle>
<small>
<table>
<TagList>
{this.props.postData.tags ? (
this.props.postData.tags.map((tag) => {
return (
<td key={this.props.postData.title + tag}>
<Tag text={tag} />
</td>
<Tag
key={this.props.postData.title + tag}
text={tag}
/>
)
})
) : (
<></>
)}
</table>
</TagList>
Published on{" "}
{this.props.postData?.date
? this.props.postData.date

View file

@ -6,7 +6,6 @@ import theming from "../theming"
import { faTag } from "@fortawesome/free-solid-svg-icons"
const StyledTag = styled.div`
display: table;
text-align: center;
padding: 0 0.8rem 0.1rem 0.8rem;

View file

@ -0,0 +1,18 @@
import React from "react"
import styled from "styled-components"
const StyledTagList = styled.div`
display: flex;
flex-wrap: wrap;
row-gap: 0.5rem;
column-gap: 0.5rem;
flex-direction: row;
justify-content: center;
`
export default class TagList extends React.Component {
render() {
// eslint-disable-next-line react/prop-types
return <StyledTagList>{this.props.children}</StyledTagList>
}
}

View file

@ -4,13 +4,14 @@ import { Helmet } from "react-helmet-async"
import { Link } from "react-router-dom"
import styled from "styled-components"
import map from "../data/map.json"
import theming from "../theming"
import Tag from "../components/Tag"
import TagList from "../components/TagList"
import NotFound from "./NotFound"
import Spinner from "../components/Spinner"
import theming from "../theming"
import map from "../data/map.json"
const StyledTitle = styled.h1`
margin-bottom: 1rem;
@ -224,7 +225,7 @@ export default class Page extends React.Component<PageProps, PageState> {
</StyledTitle>
{/* Post tags */}
<small>
<table>
<TagList>
{this.state.fetchedPage.tags ? (
this.state.fetchedPage.tags.map((tag) => {
return (
@ -241,7 +242,7 @@ export default class Page extends React.Component<PageProps, PageState> {
) : (
<></>
)}
</table>
</TagList>
{this.state.isUnsearchable ? (
<></>
) : (

View file

@ -1,16 +1,21 @@
import { useState } from "react"
import { useEffect, useState } from "react"
import styled from "styled-components"
import { BrowserRouter, useLocation, useHistory } from "react-router-dom"
import { useLocation, useHistory } from "react-router-dom"
import { Helmet } from "react-helmet-async"
import { DateRange } from "react-date-range"
import queryString from "query-string"
import queryString from "query-string" // parsing url query
import elasticlunr from "elasticlunr" // search engine
import map from "../data/map.json"
import searchIndex from "../data/search.json"
import theming from "../theming"
import "react-date-range/dist/styles.css"
import "react-date-range/dist/theme/default.css"
import theming from "../theming"
import map from "../data/map.json"
import Tag from "../components/Tag"
import TagList from "../components/TagList"
import PostCard from "../components/PostCard"
const StyledSearch = styled.div`
text-align: center;
@ -39,23 +44,18 @@ const StyledSearchControlContainer = styled.div`
}
`
const StyledSearchResult = styled.div``
const StyledTagTable = styled.table`
margin: 0 auto 0 auto;
`
// todo: find ways to get rid of wrapper component
export default function Search() {
return (
<BrowserRouter>
<_Search />
</BrowserRouter>
)
return <_Search />
}
// have to be in a separate component for tags to update when the urls change
// todo: check if using keys will fix the issue
// todo: check if using keys will allow me to use class components
function _Search() {
const [index, setIndex] = useState({} as elasticlunr.Index<unknown>)
useEffect(() => setIndex(elasticlunr.Index.load(searchIndex as never)), [])
const _history = useHistory()
const _location = useLocation()
@ -76,6 +76,10 @@ function _Search() {
},
])
const [postCards, setPostCards] = useState<unknown[]>([])
const [searchInput, setSearchInput] = useState("")
return (
<>
<Helmet>
@ -128,19 +132,20 @@ function _Search() {
/>
<StyledSearchControlContainer>
<input type="text" />
<input
type="text"
onChange={(event) =>
setSearchInput(event.target.value)
}
/>
<br />
<br />
<small>
<StyledTagTable>
<TagList>
{query.tags?.map((tag) => {
return (
<td key={tag}>
<Tag text={tag} />
</td>
)
return <Tag key={tag} text={tag} />
})}
</StyledTagTable>
</TagList>
</small>
<br />
date from: {query.from}
@ -176,10 +181,38 @@ function _Search() {
>
Search test 2
</button>
<br />
<button
onClick={() => {
try {
const _postCards: unknown[] = []
for (const res of index.search(
searchInput
)) {
if (map.posts[res.ref]) {
_postCards.push(
<PostCard
key={res.ref}
postData={{
url: res.ref,
...map.posts[res.ref],
}}
/>
)
}
setPostCards(_postCards)
}
// eslint-disable-next-line no-empty
} catch (err) {}
}}
>
Search
</button>
</StyledSearchControlContainer>
</StyledSearchContainer>
<StyledSearchResult>{map.meta.tags}</StyledSearchResult>
</StyledSearch>
{postCards}
</>
)
}

View file

@ -1795,6 +1795,11 @@
dependencies:
"@babel/types" "^7.3.0"
"@types/elasticlunr@^0.9.2":
version "0.9.2"
resolved "https://registry.yarnpkg.com/@types/elasticlunr/-/elasticlunr-0.9.2.tgz#f8ec69ff40c4538289fc4b2e0ab167bcd927265f"
integrity sha512-GAW5518ySodReGGelTacR+ei7clSN8T9+d2UjcBjGhhIcd7bMRMfalHCcKkNLFMjoqnNFmBKWPOP5w+BT27K1A==
"@types/eslint@^7.2.6":
version "7.2.10"
resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-7.2.10.tgz#4b7a9368d46c0f8cd5408c23288a59aa2394d917"
@ -4512,6 +4517,11 @@ ejs@^2.6.1:
resolved "https://registry.yarnpkg.com/ejs/-/ejs-2.7.4.tgz#48661287573dcc53e366c7a1ae52c3a120eec9ba"
integrity sha512-7vmuyh5+kuUyJKePhQfRQBhXV5Ce+RnaeeQArKu1EAMpL3WbgMt5WG6uQZpEVvYSSsxMXRKOewtDk9RaTKXRlA==
elasticlunr@^0.9.5:
version "0.9.5"
resolved "https://registry.yarnpkg.com/elasticlunr/-/elasticlunr-0.9.5.tgz#65541bb309dddd0cf94f2d1c8861b2be651bb0d5"
integrity sha1-ZVQbswnd3Qz5Ty0ciGGyvmUbsNU=
electron-to-chromium@^1.3.564, electron-to-chromium@^1.3.723:
version "1.3.727"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.727.tgz#857e310ca00f0b75da4e1db6ff0e073cc4a91ddf"