- 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 # auto generated files
source/src/data/map.json source/src/data/map.json
source/src/data/search.json
source/src/data/content/ source/src/data/content/
# dependencies # dependencies

View file

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

View file

@ -8,6 +8,7 @@
import fs from "fs" // read and write files import fs from "fs" // read and write files
import path from "path" // get relative path import path from "path" // get relative path
import elasticlunr from "elasticlunr" // search index generation
import matter from "gray-matter" // parse markdown metadata import matter from "gray-matter" // parse markdown metadata
import toc from "markdown-toc" // table of contents generation 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 // searchable data that will be converted to JSON string
const map: Map = { const map: Map = {
date: {}, date: {},
@ -79,16 +88,12 @@ const map: Map = {
series: {}, series: {},
unsearchable: {}, unsearchable: {},
} }
interface SeriesMap {
// key: url
[key: string]: {
index: number
url: string
}[]
}
const seriesMap: SeriesMap = {} 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 // converts file path to url
function path2URL(pathToConvert: string): string { function path2URL(pathToConvert: string): string {
@ -209,6 +214,11 @@ function recursiveParsePosts(fileOrFolderPath: string) {
} }
map.posts[urlPath] = postData map.posts[urlPath] = postData
index.addDoc({
title: parsedMarkdown.data.title,
body: parsedMarkdown.content,
url: urlPath,
})
} }
} }
@ -244,7 +254,12 @@ function recursiveParseUnsearchable(fileOrFolderPath: string) {
return 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 // parse markdown metadata
const parsedMarkdown = matter(fs.readFileSync(fileOrFolderPath, "utf8")) const parsedMarkdown = matter(fs.readFileSync(fileOrFolderPath, "utf8"))
@ -254,7 +269,7 @@ function recursiveParseUnsearchable(fileOrFolderPath: string) {
} }
// urlPath starts with a slash // urlPath starts with a slash
const contentFilePath = `${contentDirectoryPath}${urlPath}.json` const contentFilePath = `${contentDirectoryPath}/unsearchable${urlPath}.json`
// create directory to put json content files // create directory to put json content files
fs.mkdirSync( fs.mkdirSync(
@ -271,15 +286,15 @@ function recursiveParseUnsearchable(fileOrFolderPath: string) {
) )
// Parse data that will be written to map.js // Parse data that will be written to map.js
map.unsearchable[ map.unsearchable[urlPath] = {
urlPath.slice(
urlPath
.slice(1) // ignore the first slash
.indexOf("/") + 1
)
] = {
title: parsedMarkdown.data.title, 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 } map.series[urlPath] = { ...postData, order: [], length: 0 }
} else { } else {
map.posts[urlPath] = postData map.posts[urlPath] = postData
index.addDoc({
title: parsedMarkdown.data.title,
body: parsedMarkdown.content,
url: urlPath,
})
for (const key of Object.keys(map.series)) { for (const key of Object.keys(map.series)) {
if (urlPath.slice(0, urlPath.lastIndexOf("/")).includes(key)) { if (urlPath.slice(0, urlPath.lastIndexOf("/")).includes(key)) {
const index = parseInt( const index = parseInt(
@ -494,5 +514,5 @@ for (const seriesURL in seriesMap) {
map.series[seriesURL].order = seriesMap[seriesURL].map((item) => item.url) map.series[seriesURL].order = seriesMap[seriesURL].map((item) => item.url)
} }
// write to src/data/map.json
fs.writeFileSync(mapFilePath, JSON.stringify(map)) 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-regular-svg-icons": "^5.15.3",
"@fortawesome/free-solid-svg-icons": "^5.15.3", "@fortawesome/free-solid-svg-icons": "^5.15.3",
"@fortawesome/react-fontawesome": "^0.1.14", "@fortawesome/react-fontawesome": "^0.1.14",
"@types/elasticlunr": "^0.9.2",
"date-fns": "^2.23.0", "date-fns": "^2.23.0",
"elasticlunr": "^0.9.5",
"gray-matter": "^4.0.3", "gray-matter": "^4.0.3",
"local-storage-fallback": "^4.1.2", "local-storage-fallback": "^4.1.2",
"markdown-toc": "^1.2.0", "markdown-toc": "^1.2.0",

View file

@ -4,7 +4,9 @@ import styled from "styled-components"
import { Link } from "react-router-dom" import { Link } from "react-router-dom"
import theming from "../theming" import theming from "../theming"
import Tag from "../components/Tag" import Tag from "../components/Tag"
import TagList from "../components/TagList"
const StyledTitle = styled.h1` const StyledTitle = styled.h1`
font-size: 2rem; font-size: 2rem;
@ -68,19 +70,20 @@ export default class PostCard extends React.Component<PostCardProps> {
</StyledLink> </StyledLink>
</StyledTitle> </StyledTitle>
<small> <small>
<table> <TagList>
{this.props.postData.tags ? ( {this.props.postData.tags ? (
this.props.postData.tags.map((tag) => { this.props.postData.tags.map((tag) => {
return ( return (
<td key={this.props.postData.title + tag}> <Tag
<Tag text={tag} /> key={this.props.postData.title + tag}
</td> text={tag}
/>
) )
}) })
) : ( ) : (
<></> <></>
)} )}
</table> </TagList>
Published on{" "} Published on{" "}
{this.props.postData?.date {this.props.postData?.date
? 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" import { faTag } from "@fortawesome/free-solid-svg-icons"
const StyledTag = styled.div` const StyledTag = styled.div`
display: table;
text-align: center; text-align: center;
padding: 0 0.8rem 0.1rem 0.8rem; 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 { Link } from "react-router-dom"
import styled from "styled-components" import styled from "styled-components"
import map from "../data/map.json" import theming from "../theming"
import Tag from "../components/Tag" import Tag from "../components/Tag"
import TagList from "../components/TagList"
import NotFound from "./NotFound" import NotFound from "./NotFound"
import Spinner from "../components/Spinner" import Spinner from "../components/Spinner"
import theming from "../theming" import map from "../data/map.json"
const StyledTitle = styled.h1` const StyledTitle = styled.h1`
margin-bottom: 1rem; margin-bottom: 1rem;
@ -224,7 +225,7 @@ export default class Page extends React.Component<PageProps, PageState> {
</StyledTitle> </StyledTitle>
{/* Post tags */} {/* Post tags */}
<small> <small>
<table> <TagList>
{this.state.fetchedPage.tags ? ( {this.state.fetchedPage.tags ? (
this.state.fetchedPage.tags.map((tag) => { this.state.fetchedPage.tags.map((tag) => {
return ( return (
@ -241,7 +242,7 @@ export default class Page extends React.Component<PageProps, PageState> {
) : ( ) : (
<></> <></>
)} )}
</table> </TagList>
{this.state.isUnsearchable ? ( {this.state.isUnsearchable ? (
<></> <></>
) : ( ) : (

View file

@ -1,16 +1,21 @@
import { useState } from "react" import { useEffect, useState } from "react"
import styled from "styled-components" 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 { Helmet } from "react-helmet-async"
import { DateRange } from "react-date-range" 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/styles.css"
import "react-date-range/dist/theme/default.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 Tag from "../components/Tag"
import TagList from "../components/TagList"
import PostCard from "../components/PostCard"
const StyledSearch = styled.div` const StyledSearch = styled.div`
text-align: center; text-align: center;
@ -39,23 +44,18 @@ const StyledSearchControlContainer = styled.div`
} }
` `
const StyledSearchResult = styled.div`` // todo: find ways to get rid of wrapper component
const StyledTagTable = styled.table`
margin: 0 auto 0 auto;
`
export default function Search() { export default function Search() {
return ( return <_Search />
<BrowserRouter>
<_Search />
</BrowserRouter>
)
} }
// have to be in a separate component for tags to update when the urls change // 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() { function _Search() {
const [index, setIndex] = useState({} as elasticlunr.Index<unknown>)
useEffect(() => setIndex(elasticlunr.Index.load(searchIndex as never)), [])
const _history = useHistory() const _history = useHistory()
const _location = useLocation() const _location = useLocation()
@ -76,6 +76,10 @@ function _Search() {
}, },
]) ])
const [postCards, setPostCards] = useState<unknown[]>([])
const [searchInput, setSearchInput] = useState("")
return ( return (
<> <>
<Helmet> <Helmet>
@ -128,19 +132,20 @@ function _Search() {
/> />
<StyledSearchControlContainer> <StyledSearchControlContainer>
<input type="text" /> <input
type="text"
onChange={(event) =>
setSearchInput(event.target.value)
}
/>
<br /> <br />
<br /> <br />
<small> <small>
<StyledTagTable> <TagList>
{query.tags?.map((tag) => { {query.tags?.map((tag) => {
return ( return <Tag key={tag} text={tag} />
<td key={tag}>
<Tag text={tag} />
</td>
)
})} })}
</StyledTagTable> </TagList>
</small> </small>
<br /> <br />
date from: {query.from} date from: {query.from}
@ -176,10 +181,38 @@ function _Search() {
> >
Search test 2 Search test 2
</button> </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> </StyledSearchControlContainer>
</StyledSearchContainer> </StyledSearchContainer>
<StyledSearchResult>{map.meta.tags}</StyledSearchResult>
</StyledSearch> </StyledSearch>
{postCards}
</> </>
) )
} }

View file

@ -1795,6 +1795,11 @@
dependencies: dependencies:
"@babel/types" "^7.3.0" "@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": "@types/eslint@^7.2.6":
version "7.2.10" version "7.2.10"
resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-7.2.10.tgz#4b7a9368d46c0f8cd5408c23288a59aa2394d917" 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" resolved "https://registry.yarnpkg.com/ejs/-/ejs-2.7.4.tgz#48661287573dcc53e366c7a1ae52c3a120eec9ba"
integrity sha512-7vmuyh5+kuUyJKePhQfRQBhXV5Ce+RnaeeQArKu1EAMpL3WbgMt5WG6uQZpEVvYSSsxMXRKOewtDk9RaTKXRlA== 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: electron-to-chromium@^1.3.564, electron-to-chromium@^1.3.723:
version "1.3.727" version "1.3.727"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.727.tgz#857e310ca00f0b75da4e1db6ff0e073cc4a91ddf" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.727.tgz#857e310ca00f0b75da4e1db6ff0e073cc4a91ddf"