pompy.dev/source/src/pages/Search.tsx

350 lines
8.1 KiB
TypeScript

import { useEffect, useState, useRef } from "react"
import styled from "styled-components"
import { useLocation, useHistory } from "react-router-dom"
import { Helmet } from "react-helmet-async"
import { DateRange, Range, OnDateRangeChangeProps } from "react-date-range"
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 Tag from "../components/Tag"
import TagList from "../components/TagList"
import PostCard from "../components/PostCard"
import { Map } from "../types/typings"
const map: Map = _map
const StyledSearch = styled.div`
text-align: center;
`
const StyledSearchContainer = styled.div`
display: flex;
align-items: flex-start;
@media screen and (max-width: ${theming.size.screen_size2}) {
flex-direction: column-reverse;
}
`
const StyledSearchControlContainer = styled.div`
width: 100%;
margin-left: 1rem;
@media screen and (max-width: ${theming.size.screen_size2}) {
margin-top: 2rem;
margin-left: 0;
}
`
const StyledDateRange = styled(DateRange)`
width: 350px;
height: 350px;
@media screen and (max-width: ${theming.size.screen_size2}) {
margin-top: 1rem;
}
`
const StyledSearchBar = styled.input`
width: 100%;
border-radius: 100px; /* arbitrarily large value */
height: 3rem;
text-indent: 1rem;
font-size: 1.25rem;
outline: none;
border: ${(props) =>
theming.theme(props.theme.currentTheme, {
light: "1px solid #ccc",
dark: "1px solid #555",
})};
background-color: ${(props) =>
theming.theme(props.theme.currentTheme, {
light: theming.dark.color1,
dark: theming.dark.backgroundColor1,
})};
color: ${(props) =>
theming.theme(props.theme.currentTheme, {
light: theming.light.color1,
dark: theming.dark.color1,
})};
`
// check if post date is withing the range
function isDateInRange(
dateToCompare: string,
from: string,
to: string
): boolean {
if (!dateToCompare) throw Error("No date to compare")
const isFrom = !!from
const isTo = !!to
if (!isFrom && !isTo) return true
if (!isFrom && isTo) return Date.parse(dateToCompare) < Date.parse(to)
if (!isTo && isFrom) return Date.parse(dateToCompare) > Date.parse(from)
const compareDate = Date.parse(dateToCompare)
return Date.parse(from) < compareDate && compareDate < Date.parse(to)
}
// Search doesn't work on url change if component is not wrapped
// todo: find ways to get rid of wrapper component and use class component
export default function Search() {
return <_Search />
}
function _Search() {
const [index, setIndex] = useState({} as elasticlunr.Index<unknown>)
const inputRef = useRef<HTMLInputElement>(null)
const _history = useHistory()
const _location = useLocation()
// todo: handle duplicate/missing keys
const _query = queryString.parse(_location.search)
const query = {
from: _query.from ? _query.from?.toString() : "",
to: _query.to ? _query.to?.toString() : "",
tags: _query.tags ? _query.tags.toString().split(",") : [],
query: _query.query ? _query.query.toString() : "",
}
const defaultDateRange = [
{
startDate: new Date(0),
endDate: undefined,
key: "selection",
},
]
const [dateRange, setDateRange] = useState<Array<Range>>(defaultDateRange)
const [postCards, setPostCards] = useState<unknown[]>([])
const [searchInput, setSearchInput] = useState(query.query)
function doSearch() {
// remove focus from search bar to prevent accidental multiple search
if (document.activeElement instanceof HTMLElement)
document.activeElement.blur()
_history.push({
pathname: "/search",
search: queryString.stringify({
...(query.from && {
from: query.from,
}),
...(query.to && {
to: query.to,
}),
...(query.tags.length > 0 && {
tags: query.tags.join(","),
}),
query: searchInput,
}),
})
try {
const _postCards: unknown[] = []
for (const res of index.search(searchInput)) {
const postData = map.posts[res.ref]
// if post data exists,
// date is within range,
// and if post include tags
if (
postData &&
isDateInRange(postData.date, query.from, query.to) &&
true
) {
_postCards.push(
<PostCard
key={res.ref}
postData={{
url: res.ref,
...postData,
}}
/>
)
}
// apply search result
setPostCards(_postCards)
}
// eslint-disable-next-line no-empty
} catch (err) {}
}
// load search index only once
useEffect(() => {
setIndex(elasticlunr.Index.load(searchIndex as never))
}, [])
useEffect(() => {
doSearch()
}, [dateRange])
return (
<>
<Helmet>
<title>pomp | Search</title>
</Helmet>
<StyledSearch className="card main-content">
<h1>Search</h1>
<StyledSearchContainer>
<StyledDateRange
editableDateInputs={true}
moveRangeOnFirstSelection={false}
retainEndDateOnFirstSelection={true}
ranges={dateRange}
onChange={(item: OnDateRangeChangeProps) => {
const historyToPush = {
...(query.query && {
query: query.query,
}),
...(query.from && {
from: query.from,
}),
...(query.to && {
to: query.to,
}),
...(query.tags.length > 0 && {
tags: query.tags.join(","),
}),
}
// convert Date to YYYY-MM-DD string if it exists
if (item.selection.startDate != null)
historyToPush.from = item.selection.startDate
.toISOString()
.split("T")[0]
if (item.selection.endDate != null)
historyToPush.to = item.selection.endDate
.toISOString()
.split("T")[0]
_history.push({
pathname: "/search",
search: queryString.stringify(historyToPush),
})
setDateRange([item.selection])
}}
/>
<StyledSearchControlContainer
onSubmit={(event) => {
event.preventDefault()
}}
>
<StyledSearchBar
type="text"
ref={inputRef}
value={searchInput}
placeholder="Search"
onChange={(event) =>
setSearchInput(event.target.value)
}
onKeyPress={(e) =>
e.key === "Enter" && searchInput && doSearch()
}
/>
<br />
<br />
<small>
<TagList>
{query.tags.length > 0 &&
query.tags.map((tag) => {
return <Tag key={tag} text={tag} />
})}
</TagList>
</small>
<br />
date from: {query.from}
<br />
date to: {query.to}
<br />
<button
onClick={() => {
_history.push({
pathname: "/search",
search: queryString.stringify({
...(query.query && {
query: query.query,
}),
...(query.from && {
from: query.from,
}),
...(query.to && {
to: query.to,
}),
tags: "include,!exclude",
}),
})
}}
>
Search test 1
</button>
|
<button
onClick={() => {
_history.push({
pathname: "/search",
search: queryString.stringify({
...(query.query && {
query: query.query,
}),
...(query.from && {
from: query.from,
}),
...(query.to && {
to: query.to,
}),
tags: "include2,!exclude2",
}),
})
}}
>
Search test 2
</button>
<br />
<button
onClick={() => {
_history.push({
pathname: "/search",
search: queryString.stringify({
...(query.query && {
query: query.query,
}),
...(query.tags.length > 0 && {
tags: query.tags.join(","),
}),
}),
})
setDateRange(defaultDateRange)
}}
>
Clear date
</button>
<br />
</StyledSearchControlContainer>
</StyledSearchContainer>
</StyledSearch>
{postCards}
</>
)
}