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