- implemented basic search feature
- added tag list comonent to replace tables
This commit is contained in:
parent
06ade73ac1
commit
f731826368
10 changed files with 143 additions and 55 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
|
|
18
source/src/components/TagList.tsx
Normal file
18
source/src/components/TagList.tsx
Normal 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>
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 ? (
|
||||||
<></>
|
<></>
|
||||||
) : (
|
) : (
|
||||||
|
|
|
@ -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}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue