Ott 314 filters state on page change (#101)
parent
09ecd956f1
commit
a116bbfad2
@ -0,0 +1,3 @@ |
||||
import { createBrowserHistory } from 'history' |
||||
|
||||
export const history = createBrowserHistory() |
||||
@ -0,0 +1,6 @@ |
||||
export const filterKeys = { |
||||
DATE: 'date', |
||||
MATCH_STATUS: 'status', |
||||
SPORT_TYPE: 'sport', |
||||
TOURNAMENT_ID: 'tournament', |
||||
} |
||||
@ -0,0 +1,13 @@ |
||||
import { isValidDate } from '..' |
||||
|
||||
it('returns true for valid dates', () => { |
||||
expect(isValidDate(new Date('1970-01-01'))).toBe(true) |
||||
expect(isValidDate(new Date('2020-01-01'))).toBe(true) |
||||
expect(isValidDate(new Date('2020-08-26T08:54:32.020Z'))).toBe(true) |
||||
}) |
||||
|
||||
it('returns false for invalid dates', () => { |
||||
expect(isValidDate(new Date('2020-13-01'))).toBe(false) |
||||
expect(isValidDate(new Date('2020-12-32'))).toBe(false) |
||||
expect(isValidDate(new Date('2020-08-26T08:99:32.020Z'))).toBe(false) |
||||
}) |
||||
@ -0,0 +1,6 @@ |
||||
export const isValidDate = (value: Date) => { |
||||
if (value instanceof Date) { |
||||
return value.toString().toLocaleLowerCase() !== 'invalid date' |
||||
} |
||||
return false |
||||
} |
||||
@ -0,0 +1,15 @@ |
||||
import { MatchStatuses } from 'features/HeaderFilters/store/hooks' |
||||
|
||||
import { isValidMatchStatus } from '..' |
||||
|
||||
it('returns true for valid match statuses', () => { |
||||
expect(isValidMatchStatus(MatchStatuses.Finished)).toBe(true) |
||||
expect(isValidMatchStatus(MatchStatuses.Live)).toBe(true) |
||||
expect(isValidMatchStatus(MatchStatuses.Soon)).toBe(true) |
||||
}) |
||||
|
||||
it('returns false for invalid match statuses', () => { |
||||
expect(isValidMatchStatus(-1)).toBe(false) |
||||
expect(isValidMatchStatus(0)).toBe(false) |
||||
expect(isValidMatchStatus(4)).toBe(false) |
||||
}) |
||||
@ -0,0 +1,9 @@ |
||||
import isNumber from 'lodash/isNumber' |
||||
import includes from 'lodash/includes' |
||||
import values from 'lodash/values' |
||||
|
||||
import { MatchStatuses } from '../../hooks' |
||||
|
||||
export const isValidMatchStatus = (value: number) => ( |
||||
isNumber(value) && includes(values(MatchStatuses), value) |
||||
) |
||||
@ -0,0 +1,15 @@ |
||||
import { SportTypes } from 'config' |
||||
|
||||
import { isValidSportType } from '..' |
||||
|
||||
it('returns true for valid sport types', () => { |
||||
expect(isValidSportType(SportTypes.BASKETBALL)).toBe(true) |
||||
expect(isValidSportType(SportTypes.FOOTBALL)).toBe(true) |
||||
expect(isValidSportType(SportTypes.HOCKEY)).toBe(true) |
||||
}) |
||||
|
||||
it('returns false for invalid sport types', () => { |
||||
expect(isValidSportType(-1)).toBe(false) |
||||
expect(isValidSportType(0)).toBe(false) |
||||
expect(isValidSportType(4)).toBe(false) |
||||
}) |
||||
@ -0,0 +1,9 @@ |
||||
import isNumber from 'lodash/isNumber' |
||||
import includes from 'lodash/includes' |
||||
import values from 'lodash/values' |
||||
|
||||
import { SportTypes } from 'config' |
||||
|
||||
export const isValidSportType = (value: number | null) => ( |
||||
isNumber(value) && includes(values(SportTypes), value) |
||||
) |
||||
@ -0,0 +1,72 @@ |
||||
import forEach from 'lodash/forEach' |
||||
import size from 'lodash/size' |
||||
|
||||
import type { History } from 'history' |
||||
|
||||
import { history } from 'config/history' |
||||
|
||||
/** |
||||
* Не явно наследует от Storage (https://developer.mozilla.org/ru/docs/Web/API/Storage)
|
||||
* прямое наследование выдает ошибку: TypeError: Illegal constructor |
||||
*/ |
||||
class QueryParamStorage { |
||||
/** |
||||
* через history обновляем url строку |
||||
*/ |
||||
history: History |
||||
|
||||
/** |
||||
* хранит все состояние query param |
||||
*/ |
||||
urlParams: URLSearchParams |
||||
|
||||
constructor(historyArg: History) { |
||||
this.history = historyArg |
||||
this.urlParams = new URLSearchParams(this.history.location.search) |
||||
} |
||||
|
||||
get entries() { |
||||
const entries = this.urlParams.entries() |
||||
return Array.from(entries) |
||||
} |
||||
|
||||
updateHistory() { |
||||
history.push(`?${this.urlParams.toString()}`) |
||||
} |
||||
|
||||
clear() { |
||||
forEach(this.entries, ([key]) => { |
||||
this.urlParams.delete(key) |
||||
}) |
||||
this.updateHistory() |
||||
} |
||||
|
||||
getItem(key: string) { |
||||
return this.urlParams.get(key) |
||||
} |
||||
|
||||
key(index: number) { |
||||
const keys = this.urlParams.keys() |
||||
return Array.from(keys)[index] |
||||
} |
||||
|
||||
removeItem(key: string) { |
||||
this.urlParams.delete(key) |
||||
this.updateHistory() |
||||
} |
||||
|
||||
setItem(key: string, value: string) { |
||||
if (JSON.parse(value)) { |
||||
this.urlParams.set(key, value) |
||||
this.updateHistory() |
||||
} else { |
||||
this.removeItem(key) |
||||
} |
||||
} |
||||
|
||||
get length() { |
||||
return size(this.entries) |
||||
} |
||||
} |
||||
|
||||
export const queryParamStorage = new QueryParamStorage(history) |
||||
@ -0,0 +1,45 @@ |
||||
import isString from 'lodash/isString' |
||||
|
||||
const dateFormat = /^\d{4}-\d{2}-\d{2}/ |
||||
|
||||
/** |
||||
* Проверяет имеет ли строка формат yyyy-MM-dd |
||||
*/ |
||||
const isDateString = (value: string) => ( |
||||
isString(value) && dateFormat.test(value) |
||||
) |
||||
|
||||
/** |
||||
* Преобразовывает строку дату в объект Date |
||||
* 2020-08-26T08:17:01.604Z => 2020-08-26 |
||||
*/ |
||||
const dateReviver = (key: string, value: string) => { |
||||
if (isDateString(value)) { |
||||
return new Date(value) |
||||
} |
||||
|
||||
return value |
||||
} |
||||
|
||||
/** |
||||
* Убирает время из строки даты |
||||
* 2020-08-26T08:17:01.604Z => 2020-08-26 |
||||
*/ |
||||
export const dateReplacer = (key: string, value: string) => { |
||||
if (isDateString(value)) { |
||||
return value.slice(0, 10) |
||||
} |
||||
|
||||
return value |
||||
} |
||||
|
||||
/** |
||||
* Считывает значение из стора по key |
||||
*/ |
||||
export const readStorageInitialValue = (storage: Storage, key: string) => { |
||||
const rawValue = storage.getItem(key) |
||||
if (rawValue) { |
||||
return JSON.parse(rawValue, dateReviver) |
||||
} |
||||
return null |
||||
} |
||||
@ -0,0 +1,60 @@ |
||||
import { |
||||
useState, |
||||
useCallback, |
||||
useEffect, |
||||
} from 'react' |
||||
|
||||
import { queryParamStorage } from 'features/QueryParamsStorage' |
||||
|
||||
import { |
||||
dateReplacer, |
||||
readStorageInitialValue, |
||||
} from './helpers' |
||||
|
||||
type Args<T> = { |
||||
defaultValue: T, |
||||
key: string, |
||||
|
||||
/** |
||||
* функция для валидации полученного значения из стора |
||||
* если значение не валидно то используется defaultValue |
||||
*/ |
||||
validator?: (value: T) => boolean, |
||||
} |
||||
|
||||
/** |
||||
* Хук высшего порядка, получает сторедж |
||||
* и возвращает хук работающий с данным стореджем |
||||
* Хук считывает значение из стора и использует как начальное значение стейта, |
||||
* при вызове сеттера обновляется сторедж и реакт стейет |
||||
*/ |
||||
const createHook = (storage: Storage) => ( |
||||
<T extends any>({ |
||||
defaultValue, |
||||
key, |
||||
validator, |
||||
}: Args<T>) => { |
||||
const storeValue = readStorageInitialValue(storage, key) |
||||
const isValid = validator && validator(storeValue) |
||||
const initialState = isValid ? storeValue : defaultValue |
||||
|
||||
const [state, setState] = useState<T>(initialState) |
||||
|
||||
const setStateAndSave = useCallback((value: T) => { |
||||
storage.setItem(key, JSON.stringify(value, dateReplacer)) |
||||
setState(value) |
||||
}, [key]) |
||||
|
||||
useEffect(() => { |
||||
if (!isValid) { |
||||
storage.removeItem(key) |
||||
} |
||||
}, [isValid, key]) |
||||
|
||||
return [state, setStateAndSave] as const |
||||
} |
||||
) |
||||
|
||||
export const useLocalStore = createHook(localStorage) |
||||
export const useSessionStore = createHook(sessionStorage) |
||||
export const useQueryParamStore = createHook(queryParamStorage) |
||||
Loading…
Reference in new issue