From 9b4ef41030131e2b6eaa5c8c0001f164b4aa3514 Mon Sep 17 00:00:00 2001 From: dm1sh Date: Thu, 20 Jul 2023 14:58:05 +0300 Subject: [PATCH] Added query filters getting and setting on homepage fixes #27 --- front/src/hooks/index.ts | 3 ++- front/src/hooks/useFilters.ts | 47 +++++++++++++++++++++++++++++++++++ front/src/pages/HomePage.tsx | 9 +++---- front/src/utils/filters.ts | 19 +++++++++++++- front/src/utils/types.ts | 12 ++++++++- 5 files changed, 82 insertions(+), 8 deletions(-) create mode 100644 front/src/hooks/useFilters.ts diff --git a/front/src/hooks/index.ts b/front/src/hooks/index.ts index 2e5d398..dd1b5ff 100644 --- a/front/src/hooks/index.ts +++ b/front/src/hooks/index.ts @@ -1,4 +1,5 @@ export { default as useStoryDimensions } from './useStoryDimensions' export { default as useSend } from './useSend' export { default as useFetch } from './useFetch' -export { default as UseStoryIndex } from './useStoryIndex' +export { default as useStoryIndex } from './useStoryIndex' +export { default as useFilters } from './useFilters' diff --git a/front/src/hooks/useFilters.ts b/front/src/hooks/useFilters.ts new file mode 100644 index 0000000..2394b13 --- /dev/null +++ b/front/src/hooks/useFilters.ts @@ -0,0 +1,47 @@ +import { useEffect, useState } from 'react' +import { useSearchParams } from 'react-router-dom' +import { FiltersType, URLDecoreFilters, URLEncodeFilters, defaultFilters } from '../utils/filters' +import { SetState } from '../utils/types' + +function useFilters(initialFilters: FiltersType = defaultFilters): [FiltersType, SetState] { + const [searchParams, setSearchParams] = useSearchParams() + + const [filters, setFilters] = useState(initialFilters) + + const appendFiltersSearchParams = (filters: FiltersType) => ( + setSearchParams(params => ({ + ...Object.fromEntries(params), + ...URLEncodeFilters(filters) + }), { replace: true }) + ) + + useEffect(() => { + const urlFilters = URLDecoreFilters(searchParams) + + setFilters(prev => ({ + ...prev, + ...urlFilters, + })) + + appendFiltersSearchParams({ + ...URLEncodeFilters(initialFilters), + ...URLEncodeFilters(urlFilters), + }) + // searchParams have actual query string at first render + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + + const withQuery = (f: SetState) => ( + (nextInit: (FiltersType | ((prev: FiltersType) => FiltersType))) => { + const newFilters = (typeof nextInit === 'function') ? nextInit(filters) : nextInit + + appendFiltersSearchParams(newFilters) + + f(nextInit) + } + ) + + return [filters, withQuery(setFilters)] +} + +export default useFilters diff --git a/front/src/pages/HomePage.tsx b/front/src/pages/HomePage.tsx index 2c4d36d..14afa98 100644 --- a/front/src/pages/HomePage.tsx +++ b/front/src/pages/HomePage.tsx @@ -3,15 +3,14 @@ import Stories from 'react-insta-stories' import { Story } from 'react-insta-stories/dist/interfaces' import { BottomNavBar, AnnouncementDetails, Filters } from '../components' -import { useStoryDimensions } from '../hooks' +import { useFilters, useStoryDimensions } from '../hooks' import { useAnnouncements } from '../hooks/api' -import { defaultFilters } from '../utils/filters' import { Announcement } from '../api/announcement/types' import { categoryGraphics } from '../assets/category' +import { UseFetchReturn, gotError } from '../hooks/useFetch' +import { useStoryIndex } from '../hooks' import puffSpinner from '../assets/puff.svg' -import { UseFetchReturn, gotError } from '../hooks/useFetch' -import useStoryIndex from '../hooks/useStoryIndex' function generateStories(announcements: Announcement[]): Story[] { return announcements.map(announcement => { @@ -67,8 +66,8 @@ function HomePage() { const { height, width } = useStoryDimensions(16 / 9) const [filterShown, setFilterShown] = useState(false) - const [filter, setFilter] = useState(defaultFilters) + const [filter, setFilter] = useFilters() const announcements = useAnnouncements(filter) diff --git a/front/src/utils/filters.ts b/front/src/utils/filters.ts index ca4b6f0..50039d6 100644 --- a/front/src/utils/filters.ts +++ b/front/src/utils/filters.ts @@ -1,4 +1,6 @@ import { Announcement } from '../api/announcement/types' +import { isCategory } from '../assets/category' +import { fallbackToUndefined, isInt } from './types' const filterNames = ['userId', 'category', 'metro', 'bookedBy'] as const type FilterNames = typeof filterNames[number] @@ -21,5 +23,20 @@ const URLEncodeFilters = (filters: FiltersType) => ( ) ) +const URLDecoreFilters = (params: URLSearchParams): FiltersType => { + const strFilters = Object.fromEntries( + filterNames.map( + fName => [fName, params.get(fName)] + ).filter((p): p is [string, string] => p[1] !== null) + ) + + return { + bookedBy: fallbackToUndefined(strFilters['bookedBy'], isInt), + category: fallbackToUndefined(strFilters['category'], isCategory), + metro: strFilters['metro'], + userId: fallbackToUndefined(strFilters['userId'], isInt) + } +} + export type { FilterNames, FiltersType } -export { defaultFilters, filterNames, URLEncodeFilters } +export { defaultFilters, filterNames, URLEncodeFilters, URLDecoreFilters } diff --git a/front/src/utils/types.ts b/front/src/utils/types.ts index 4ac3255..8f42cfa 100644 --- a/front/src/utils/types.ts +++ b/front/src/utils/types.ts @@ -60,8 +60,18 @@ const isString = (obj: unknown): obj is string => ( typeof obj === 'string' ) +const isInt = (obj: unknown): obj is number => ( + Number.isSafeInteger(obj) +) + +function fallbackToUndefined(obj: unknown, isT: ((obj: unknown) => obj is T)) { + if (!isT(obj)) return undefined + + return obj +} + type SetState = React.Dispatch> export type { SetState } -export { isRecord, isObject, isConst, isLiteralUnion, isArrayOf, isString } +export { isRecord, isObject, isConst, isLiteralUnion, isArrayOf, isString, isInt, fallbackToUndefined }