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:
parent
8d61768907
commit
d731ec1b5f
4 changed files with 159 additions and 191 deletions
29
source/src/pages/Search/DateRange.tsx
Normal file
29
source/src/pages/Search/DateRange.tsx
Normal 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;
|
||||||
|
`
|
|
@ -1,12 +1,10 @@
|
||||||
import styled, { ThemeConsumer } from "styled-components"
|
import styled, { ThemeConsumer } from "styled-components"
|
||||||
import { useNavigate, useSearchParams } from "react-router-dom"
|
|
||||||
import Select from "react-select"
|
import Select from "react-select"
|
||||||
|
|
||||||
import theming from "../../styles/theming"
|
import theming from "../../styles/theming"
|
||||||
|
|
||||||
import { Query } from "."
|
|
||||||
import { Map } from "../../../types/typing"
|
|
||||||
import _map from "../../data/map.json"
|
import _map from "../../data/map.json"
|
||||||
|
import { Map } from "../../../types/typing"
|
||||||
|
|
||||||
const map: Map = _map
|
const map: Map = _map
|
||||||
|
|
||||||
|
@ -20,24 +18,18 @@ export interface TagsData {
|
||||||
label: string
|
label: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const options: TagsData[] = [
|
const options: TagsData[] = map.meta.tags.map((elem) => ({
|
||||||
...map.meta.tags.map((elem) => ({ value: elem, label: elem })),
|
value: elem,
|
||||||
]
|
label: elem,
|
||||||
|
}))
|
||||||
|
|
||||||
interface TagSelectProps {
|
interface TagSelectProps {
|
||||||
query: Query
|
defaultValue: TagsData[]
|
||||||
selectedTags?: TagsData[]
|
onChange(newValue: unknown): void
|
||||||
setSelectedOption: React.Dispatch<
|
|
||||||
React.SetStateAction<TagsData[] | undefined>
|
|
||||||
>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const TagSelect = (props: TagSelectProps) => {
|
const TagSelect = (props: TagSelectProps) => {
|
||||||
const { query, selectedTags, setSelectedOption } = props
|
const { onChange, defaultValue: selectedTags } = props
|
||||||
|
|
||||||
const navigate = useNavigate()
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
const [_, setSearchParams] = useSearchParams()
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledReactTagsContainer>
|
<StyledReactTagsContainer>
|
||||||
|
@ -133,31 +125,7 @@ const TagSelect = (props: TagSelectProps) => {
|
||||||
}),
|
}),
|
||||||
}}
|
}}
|
||||||
defaultValue={selectedTags}
|
defaultValue={selectedTags}
|
||||||
onChange={(newSelectedTags) => {
|
onChange={onChange}
|
||||||
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,
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
options={options}
|
options={options}
|
||||||
isMulti
|
isMulti
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,23 +1,24 @@
|
||||||
/* eslint-disable react/prop-types */
|
/* eslint-disable react/prop-types */
|
||||||
|
|
||||||
import { useEffect, useState, useRef } from "react"
|
import { useEffect, useState } from "react"
|
||||||
import styled from "styled-components"
|
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 { 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 elasticlunr from "elasticlunr" // search engine
|
||||||
|
|
||||||
import _map from "../../data/map.json"
|
import _map from "../../data/map.json"
|
||||||
import searchData from "../../data/search.json"
|
import searchData from "../../data/search.json"
|
||||||
import theming from "../../styles/theming"
|
import theming from "../../styles/theming"
|
||||||
|
|
||||||
|
import Loading from "../../components/Loading"
|
||||||
import PostCard from "../../components/PostCard"
|
import PostCard from "../../components/PostCard"
|
||||||
import MainContent from "../../components/MainContent"
|
import MainContent from "../../components/MainContent"
|
||||||
|
|
||||||
import SearchBar from "./SearchBar"
|
import SearchBar from "./SearchBar"
|
||||||
import TagSelect, { TagsData } from "./TagSelect"
|
import TagSelect, { TagsData } from "./TagSelect"
|
||||||
|
import { ClearDateButton, DateRangeControl, StyledDateRange } from "./DateRange"
|
||||||
|
|
||||||
import { Map } from "../../../types/typing"
|
import { Map } from "../../../types/typing"
|
||||||
|
|
||||||
|
@ -28,42 +29,25 @@ const map: Map = _map
|
||||||
|
|
||||||
const searchIndex = elasticlunr.Index.load(searchData as never)
|
const searchIndex = elasticlunr.Index.load(searchData as never)
|
||||||
|
|
||||||
export interface Query {
|
export interface SearchParams {
|
||||||
from: string
|
date_from: string
|
||||||
to: string
|
date_to: string
|
||||||
tags: string[]
|
tags: string[]
|
||||||
query: string
|
query: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const defaultDateRange = [
|
||||||
|
{
|
||||||
|
startDate: undefined,
|
||||||
|
endDate: undefined,
|
||||||
|
key: "selection",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
const StyledSearch = styled(MainContent)`
|
const StyledSearch = styled(MainContent)`
|
||||||
text-align: center;
|
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`
|
const StyledSearchContainer = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
|
@ -85,22 +69,20 @@ const StyledSearchControlContainer = styled.div`
|
||||||
`
|
`
|
||||||
|
|
||||||
// check if post date is withing the range
|
// check if post date is withing the range
|
||||||
function isDateInRange(
|
function isDateInRange(dateStringToCompare: string, range: Range): boolean {
|
||||||
dateToCompare: string,
|
if (!dateStringToCompare) throw Error("No date to compare")
|
||||||
from: string,
|
const dateToCompare = new Date(dateStringToCompare)
|
||||||
to: string
|
const { startDate, endDate } = range
|
||||||
): boolean {
|
|
||||||
if (!dateToCompare) throw Error("No date to compare")
|
|
||||||
|
|
||||||
const isFrom = !!from
|
const startDateExists = !!startDate
|
||||||
const isTo = !!to
|
const endDateExists = !!endDate
|
||||||
|
|
||||||
if (!isFrom && !isTo) return true
|
if (endDateExists && !startDateExists) return dateToCompare < endDate
|
||||||
if (!isFrom && isTo) return Date.parse(dateToCompare) < Date.parse(to)
|
if (startDateExists && !endDateExists) return dateToCompare > startDate
|
||||||
if (!isTo && isFrom) return Date.parse(dateToCompare) > Date.parse(from)
|
if (startDateExists && endDateExists)
|
||||||
|
return dateToCompare > startDate && dateToCompare < endDate
|
||||||
|
|
||||||
const compareDate = Date.parse(dateToCompare)
|
return true
|
||||||
return Date.parse(from) < compareDate && compareDate < Date.parse(to)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function isSelectedTagsInPost(selectedTags?: TagsData[], postTags?: string[]) {
|
function isSelectedTagsInPost(selectedTags?: TagsData[], postTags?: string[]) {
|
||||||
|
@ -114,57 +96,94 @@ function isSelectedTagsInPost(selectedTags?: TagsData[], postTags?: string[]) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Search doesn't work on url change if component is not wrapped
|
|
||||||
const Search = () => {
|
const Search = () => {
|
||||||
const inputRef = useRef<HTMLInputElement | null>(null)
|
// URL search parameters
|
||||||
|
const [URLSearchParams, setURLSearchParams] = useSearchParams()
|
||||||
|
|
||||||
const navigate = useNavigate()
|
const [initialized, setInitialized] = useState(false)
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
const [_, setSearchParams] = useSearchParams()
|
|
||||||
const _location = useLocation()
|
|
||||||
|
|
||||||
// todo: handle duplicate/missing keys
|
const [dateRange, setDateRange] = useState<Range[]>(defaultDateRange)
|
||||||
const _query = queryString.parse(_location.search)
|
const [selectedTags, setSelectedTags] = useState<TagsData[]>([])
|
||||||
|
const [searchInput, setSearchInput] = useState("")
|
||||||
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 [postCards, setPostCards] = useState<unknown[]>([])
|
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() {
|
// parse search parameters
|
||||||
navigate("/search")
|
useEffect(() => {
|
||||||
setSearchParams({
|
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 && {
|
...(searchInput && {
|
||||||
query: 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 {
|
try {
|
||||||
const _postCards: unknown[] = []
|
const _postCards: unknown[] = []
|
||||||
for (const res of searchIndex.search(searchInput)) {
|
for (const res of searchIndex.search(searchInput)) {
|
||||||
|
@ -172,7 +191,7 @@ const Search = () => {
|
||||||
|
|
||||||
if (
|
if (
|
||||||
postData && // if post data exists
|
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
|
isSelectedTagsInPost(selectedTags, postData.tags) // if post include tags
|
||||||
) {
|
) {
|
||||||
_postCards.push(
|
_postCards.push(
|
||||||
|
@ -186,9 +205,9 @@ const Search = () => {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// apply search result
|
// apply search result
|
||||||
setPostCards(_postCards)
|
setPostCards(_postCards)
|
||||||
// todo: set _postCards.length
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-empty
|
// eslint-disable-next-line no-empty
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
@ -196,62 +215,7 @@ const Search = () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
if (!initialized) return <Loading />
|
||||||
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])
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -264,7 +228,11 @@ const Search = () => {
|
||||||
|
|
||||||
<StyledSearchContainer>
|
<StyledSearchContainer>
|
||||||
<DateRangeControl>
|
<DateRangeControl>
|
||||||
<ClearDateButton onClick={clearDate}>
|
<ClearDateButton
|
||||||
|
onClick={() => {
|
||||||
|
setDateRange(defaultDateRange)
|
||||||
|
}}
|
||||||
|
>
|
||||||
Reset range
|
Reset range
|
||||||
</ClearDateButton>
|
</ClearDateButton>
|
||||||
<StyledDateRange
|
<StyledDateRange
|
||||||
|
@ -272,7 +240,9 @@ const Search = () => {
|
||||||
retainEndDateOnFirstSelection
|
retainEndDateOnFirstSelection
|
||||||
moveRangeOnFirstSelection={false}
|
moveRangeOnFirstSelection={false}
|
||||||
ranges={dateRange}
|
ranges={dateRange}
|
||||||
onChange={onDateRangeChange}
|
onChange={(rangesByKey) => {
|
||||||
|
setDateRange([rangesByKey.selection])
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</DateRangeControl>
|
</DateRangeControl>
|
||||||
|
|
||||||
|
@ -281,8 +251,7 @@ const Search = () => {
|
||||||
>
|
>
|
||||||
<SearchBar
|
<SearchBar
|
||||||
autoFocus
|
autoFocus
|
||||||
type="text"
|
type="search"
|
||||||
ref={inputRef}
|
|
||||||
value={searchInput}
|
value={searchInput}
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
placeholder="Search"
|
placeholder="Search"
|
||||||
|
@ -298,9 +267,10 @@ const Search = () => {
|
||||||
{postCards.length}{" "}
|
{postCards.length}{" "}
|
||||||
{postCards.length > 1 ? "results" : "result"}
|
{postCards.length > 1 ? "results" : "result"}
|
||||||
<TagSelect
|
<TagSelect
|
||||||
query={query}
|
defaultValue={selectedTags}
|
||||||
selectedTags={selectedTags}
|
onChange={(newValue) => {
|
||||||
setSelectedOption={setSelectedOption}
|
setSelectedTags(newValue as TagsData[])
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</StyledSearchControlContainer>
|
</StyledSearchControlContainer>
|
||||||
</StyledSearchContainer>
|
</StyledSearchContainer>
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"allowSyntheticDefaultImports": true,
|
"allowSyntheticDefaultImports": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
|
"downlevelIteration": true,
|
||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
"noImplicitAny": true,
|
"noImplicitAny": true,
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue