diff --git a/source/src/pages/Search/DateRange.tsx b/source/src/pages/Search/DateRange.tsx new file mode 100644 index 0000000..12c4a67 --- /dev/null +++ b/source/src/pages/Search/DateRange.tsx @@ -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; +` diff --git a/source/src/pages/Search/TagSelect.tsx b/source/src/pages/Search/TagSelect.tsx index 7d063f0..8ccd17a 100644 --- a/source/src/pages/Search/TagSelect.tsx +++ b/source/src/pages/Search/TagSelect.tsx @@ -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 - > + 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 ( @@ -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 /> diff --git a/source/src/pages/Search/index.tsx b/source/src/pages/Search/index.tsx index 806ea3c..d5d1781 100644 --- a/source/src/pages/Search/index.tsx +++ b/source/src/pages/Search/index.tsx @@ -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(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(defaultDateRange) + const [selectedTags, setSelectedTags] = useState([]) + const [searchInput, setSearchInput] = useState("") const [postCards, setPostCards] = useState([]) - const [dateRange, setDateRange] = useState>(defaultDateRange) - const [searchInput, setSearchInput] = useState(query.query) - const [selectedTags, setSelectedOption] = useState( - 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 return ( <> @@ -264,7 +228,11 @@ const Search = () => { - + { + setDateRange(defaultDateRange) + }} + > Reset range { retainEndDateOnFirstSelection moveRangeOnFirstSelection={false} ranges={dateRange} - onChange={onDateRangeChange} + onChange={(rangesByKey) => { + setDateRange([rangesByKey.selection]) + }} /> @@ -281,8 +251,7 @@ const Search = () => { > { {postCards.length}{" "} {postCards.length > 1 ? "results" : "result"} { + setSelectedTags(newValue as TagsData[]) + }} /> diff --git a/source/tsconfig.json b/source/tsconfig.json index 778292f..f4c0f8f 100644 --- a/source/tsconfig.json +++ b/source/tsconfig.json @@ -8,6 +8,7 @@ "esModuleInterop": true, "allowSyntheticDefaultImports": true, "strict": true, + "downlevelIteration": true, "forceConsistentCasingInFileNames": true, "noFallthroughCasesInSwitch": true, "noImplicitAny": true,