Search page update

- streamlined Search logic
- splitted even more code
- made searchbox type "search"
    - show appropriate icons in mobile keyboard
This commit is contained in:
Kim, Jimin 2021-12-21 13:59:58 +09:00
parent 8d61768907
commit d731ec1b5f
4 changed files with 159 additions and 191 deletions

View file

@ -0,0 +1,29 @@
import { DateRange } from "react-date-range"
import styled from "styled-components"
import theming from "../../styles/theming"
export const DateRangeControl = styled.div`
width: 350px;
@media screen and (max-width: ${theming.size.screen_size2}) {
margin-top: 2rem;
}
`
export const ClearDateButton = styled.button`
width: 100%;
height: 2.5rem;
border: none;
cursor: pointer;
background-color: tomato; /* 🍅 mmm tomato 🍅 */
color: white;
font-weight: bold;
`
export const StyledDateRange = styled(DateRange)`
width: 100%;
height: 350px;
`

View file

@ -1,12 +1,10 @@
import styled, { ThemeConsumer } from "styled-components"
import { useNavigate, useSearchParams } from "react-router-dom"
import Select from "react-select"
import theming from "../../styles/theming"
import { Query } from "."
import { Map } from "../../../types/typing"
import _map from "../../data/map.json"
import { Map } from "../../../types/typing"
const map: Map = _map
@ -20,24 +18,18 @@ export interface TagsData {
label: string
}
const options: TagsData[] = [
...map.meta.tags.map((elem) => ({ value: elem, label: elem })),
]
const options: TagsData[] = map.meta.tags.map((elem) => ({
value: elem,
label: elem,
}))
interface TagSelectProps {
query: Query
selectedTags?: TagsData[]
setSelectedOption: React.Dispatch<
React.SetStateAction<TagsData[] | undefined>
>
defaultValue: TagsData[]
onChange(newValue: unknown): void
}
const TagSelect = (props: TagSelectProps) => {
const { query, selectedTags, setSelectedOption } = props
const navigate = useNavigate()
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [_, setSearchParams] = useSearchParams()
const { onChange, defaultValue: selectedTags } = props
return (
<StyledReactTagsContainer>
@ -133,31 +125,7 @@ const TagSelect = (props: TagSelectProps) => {
}),
}}
defaultValue={selectedTags}
onChange={(newSelectedTags) => {
setSelectedOption(newSelectedTags as TagsData[])
navigate("/search")
const tags =
newSelectedTags
.map((elem) => elem.value)
.join(",") || undefined
setSearchParams({
...(query.query && {
query: query.query,
}),
...(query.from && {
from: query.from,
}),
...(query.to && {
to: query.to,
}),
...(tags && {
tags: tags,
}),
})
}}
onChange={onChange}
options={options}
isMulti
/>

View file

@ -1,23 +1,24 @@
/* eslint-disable react/prop-types */
import { useEffect, useState, useRef } from "react"
import { useEffect, useState } from "react"
import styled from "styled-components"
import { useLocation, useNavigate, useSearchParams } from "react-router-dom"
import { useSearchParams } from "react-router-dom"
import { Helmet } from "react-helmet-async"
import { DateRange, Range } from "react-date-range"
import { Range } 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 searchData from "../../data/search.json"
import theming from "../../styles/theming"
import Loading from "../../components/Loading"
import PostCard from "../../components/PostCard"
import MainContent from "../../components/MainContent"
import SearchBar from "./SearchBar"
import TagSelect, { TagsData } from "./TagSelect"
import { ClearDateButton, DateRangeControl, StyledDateRange } from "./DateRange"
import { Map } from "../../../types/typing"
@ -28,42 +29,25 @@ const map: Map = _map
const searchIndex = elasticlunr.Index.load(searchData as never)
export interface Query {
from: string
to: string
export interface SearchParams {
date_from: string
date_to: string
tags: string[]
query: string
}
const defaultDateRange = [
{
startDate: undefined,
endDate: undefined,
key: "selection",
},
]
const StyledSearch = styled(MainContent)`
text-align: center;
`
const DateRangeControl = styled.div`
width: 350px;
@media screen and (max-width: ${theming.size.screen_size2}) {
margin-top: 2rem;
}
`
const ClearDateButton = styled.button`
width: 100%;
height: 2.5rem;
border: none;
cursor: pointer;
background-color: tomato; /* 🍅 mmm tomato 🍅 */
color: white;
font-weight: bold;
`
const StyledDateRange = styled(DateRange)`
width: 100%;
height: 350px;
`
const StyledSearchContainer = styled.div`
display: flex;
align-items: flex-start;
@ -85,22 +69,20 @@ const StyledSearchControlContainer = styled.div`
`
// 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")
function isDateInRange(dateStringToCompare: string, range: Range): boolean {
if (!dateStringToCompare) throw Error("No date to compare")
const dateToCompare = new Date(dateStringToCompare)
const { startDate, endDate } = range
const isFrom = !!from
const isTo = !!to
const startDateExists = !!startDate
const endDateExists = !!endDate
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)
if (endDateExists && !startDateExists) return dateToCompare < endDate
if (startDateExists && !endDateExists) return dateToCompare > startDate
if (startDateExists && endDateExists)
return dateToCompare > startDate && dateToCompare < endDate
const compareDate = Date.parse(dateToCompare)
return Date.parse(from) < compareDate && compareDate < Date.parse(to)
return true
}
function isSelectedTagsInPost(selectedTags?: TagsData[], postTags?: string[]) {
@ -114,57 +96,94 @@ function isSelectedTagsInPost(selectedTags?: TagsData[], postTags?: string[]) {
return true
}
// Search doesn't work on url change if component is not wrapped
const Search = () => {
const inputRef = useRef<HTMLInputElement | null>(null)
// URL search parameters
const [URLSearchParams, setURLSearchParams] = useSearchParams()
const navigate = useNavigate()
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [_, setSearchParams] = useSearchParams()
const _location = useLocation()
const [initialized, setInitialized] = useState(false)
// todo: handle duplicate/missing keys
const _query = queryString.parse(_location.search)
const query: 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: undefined,
endDate: undefined,
key: "selection",
},
]
const [dateRange, setDateRange] = useState<Range[]>(defaultDateRange)
const [selectedTags, setSelectedTags] = useState<TagsData[]>([])
const [searchInput, setSearchInput] = useState("")
const [postCards, setPostCards] = useState<unknown[]>([])
const [dateRange, setDateRange] = useState<Array<Range>>(defaultDateRange)
const [searchInput, setSearchInput] = useState(query.query)
const [selectedTags, setSelectedOption] = useState<TagsData[] | undefined>(
query.tags.map((elem) => ({ label: elem, value: elem }))
)
function doSearch() {
navigate("/search")
setSearchParams({
// parse search parameters
useEffect(() => {
for (const [key, value] of URLSearchParams.entries()) {
switch (key) {
case "date_from":
setDateRange((prev) => [
{ ...prev[0], startDate: new Date(value) },
])
break
case "date_to":
setDateRange((prev) => [
{ ...prev[0], endDate: new Date(value) },
])
break
case "tags":
setSelectedTags(
value.split(",").map((elem) => {
return { value: elem, label: elem }
})
)
break
case "query":
setSearchInput(value)
break
}
}
setInitialized(true)
}, [])
// update URL when data changes
useEffect(() => {
let date_from
let date_to
// convert Date to YYYY-MM-DD string if it exists
if (dateRange[0].startDate)
date_from = dateRange[0].startDate.toISOString().split("T")[0]
if (dateRange[0].endDate)
date_to = dateRange[0].endDate.toISOString().split("T")[0]
setURLSearchParams({
...(date_from && {
date_from: date_from,
}),
...(date_to && {
date_to: date_to,
}),
...(selectedTags.length > 0 && {
tags: selectedTags.map((value) => value.value).join(","),
}),
...(searchInput && {
query: searchInput,
}),
...(query.from && {
from: query.from,
}),
...(query.to && {
to: query.to,
}),
...(query.tags.length > 0 && {
tags: query.tags.join(","),
}),
})
}, [dateRange, selectedTags, searchInput])
// run search if date range and selected tags change
useEffect(() => {
doSearch()
}, [dateRange, selectedTags])
// run search if user stops typing
useEffect(() => {
const delayDebounceFn = setTimeout(() => {
doSearch()
}, 200)
return () => clearTimeout(delayDebounceFn)
}, [searchInput])
function doSearch() {
try {
const _postCards: unknown[] = []
for (const res of searchIndex.search(searchInput)) {
@ -172,7 +191,7 @@ const Search = () => {
if (
postData && // if post data exists
isDateInRange(postData.date, query.from, query.to) && // date is within range
isDateInRange(postData.date, dateRange[0]) && // date is within range
isSelectedTagsInPost(selectedTags, postData.tags) // if post include tags
) {
_postCards.push(
@ -186,9 +205,9 @@ const Search = () => {
)
}
}
// apply search result
setPostCards(_postCards)
// todo: set _postCards.length
// eslint-disable-next-line no-empty
} catch (err) {
@ -196,62 +215,7 @@ const Search = () => {
}
}
useEffect(() => {
doSearch()
}, [dateRange, selectedTags])
useEffect(() => {
const delayDebounceFn = setTimeout(() => {
doSearch()
}, 200)
return () => clearTimeout(delayDebounceFn)
}, [searchInput])
function clearDate() {
navigate("/search")
setSearchParams({
...(query.query && {
query: query.query,
}),
...(query.tags.length > 0 && {
tags: query.tags.join(","),
}),
})
setDateRange(defaultDateRange)
}
function onDateRangeChange(item: { [key: string]: Range }) {
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)
historyToPush.from = item.selection.startDate
.toISOString()
.split("T")[0]
if (item.selection.endDate)
historyToPush.to = item.selection.endDate
.toISOString()
.split("T")[0]
navigate("/search")
setSearchParams(historyToPush)
setDateRange([item.selection])
}
if (!initialized) return <Loading />
return (
<>
@ -264,7 +228,11 @@ const Search = () => {
<StyledSearchContainer>
<DateRangeControl>
<ClearDateButton onClick={clearDate}>
<ClearDateButton
onClick={() => {
setDateRange(defaultDateRange)
}}
>
Reset range
</ClearDateButton>
<StyledDateRange
@ -272,7 +240,9 @@ const Search = () => {
retainEndDateOnFirstSelection
moveRangeOnFirstSelection={false}
ranges={dateRange}
onChange={onDateRangeChange}
onChange={(rangesByKey) => {
setDateRange([rangesByKey.selection])
}}
/>
</DateRangeControl>
@ -281,8 +251,7 @@ const Search = () => {
>
<SearchBar
autoFocus
type="text"
ref={inputRef}
type="search"
value={searchInput}
autoComplete="off"
placeholder="Search"
@ -298,9 +267,10 @@ const Search = () => {
{postCards.length}{" "}
{postCards.length > 1 ? "results" : "result"}
<TagSelect
query={query}
selectedTags={selectedTags}
setSelectedOption={setSelectedOption}
defaultValue={selectedTags}
onChange={(newValue) => {
setSelectedTags(newValue as TagsData[])
}}
/>
</StyledSearchControlContainer>
</StyledSearchContainer>

View file

@ -8,6 +8,7 @@
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"downlevelIteration": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"noImplicitAny": true,