Ott 314 filters state on page change (#101)

keep-around/af30b88d367751c9e05a735e4a0467a96238ef47
Mirlan 5 years ago committed by GitHub
parent 09ecd956f1
commit a116bbfad2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      src/config/history.tsx
  2. 1
      src/config/index.tsx
  3. 5
      src/features/App/index.tsx
  4. 6
      src/features/HeaderFilters/store/config.tsx
  5. 13
      src/features/HeaderFilters/store/helpers/isValidDate/__tests__/index.tsx
  6. 6
      src/features/HeaderFilters/store/helpers/isValidDate/index.tsx
  7. 15
      src/features/HeaderFilters/store/helpers/isValidMatchStatus/__tests__/index.tsx
  8. 9
      src/features/HeaderFilters/store/helpers/isValidMatchStatus/index.tsx
  9. 15
      src/features/HeaderFilters/store/helpers/isValidSportType/__tests__/index.tsx
  10. 9
      src/features/HeaderFilters/store/helpers/isValidSportType/index.tsx
  11. 70
      src/features/HeaderFilters/store/hooks/index.tsx
  12. 72
      src/features/QueryParamsStorage/index.tsx
  13. 13
      src/features/ToggleScore/store.tsx
  14. 1
      src/hooks/index.tsx
  15. 45
      src/hooks/useStorage/helpers.tsx
  16. 60
      src/hooks/useStorage/index.tsx
  17. 4
      src/hooks/useToggle.tsx

@ -0,0 +1,3 @@
import { createBrowserHistory } from 'history'
export const history = createBrowserHistory()

@ -4,3 +4,4 @@ export * from './authKeys'
export * from './procedures'
export * from './sportTypes'
export * from './profileTypes'
export * from './history'

@ -1,6 +1,7 @@
import React from 'react'
import { Router } from 'react-router-dom'
import { createBrowserHistory } from 'history'
import { history } from 'config'
import { GlobalStores } from 'features/GlobalStores'
import { useAuthStore } from 'features/AuthStore'
@ -11,8 +12,6 @@ import { Theme } from 'features/Theme'
import { AuthenticatedApp } from './AuthenticatedApp'
import { UnauthenticatedApp } from './UnauthenticatedApp'
const history = createBrowserHistory()
const Main = () => {
const { token } = useAuthStore()

@ -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)
)

@ -10,15 +10,23 @@ import flatten from 'lodash/flatten'
import pipe from 'lodash/fp/pipe'
import fpMap from 'lodash/fp/map'
import fpOrderBy from 'lodash/fp/orderBy'
import isNumber from 'lodash/isNumber'
import format from 'date-fns/format'
import startOfDay from 'date-fns/startOfDay'
import type { Matches, Content } from 'requests'
import { SportTypes, SPORT_NAMES } from 'config'
import { getMatches } from 'requests'
import { useLexicsStore } from 'features/LexicsStore'
import { SportTypes, SPORT_NAMES } from 'config'
import { getProfileLogo } from 'helpers'
import { useQueryParamStore } from 'hooks'
import { useLexicsStore } from 'features/LexicsStore'
import { filterKeys } from '../config'
import { isValidDate } from '../helpers/isValidDate'
import { isValidSportType } from '../helpers/isValidSportType'
import { isValidMatchStatus } from '../helpers/isValidMatchStatus'
type Name = 'name_rus' | 'name_eng'
export type Match = {
@ -49,10 +57,39 @@ export const useFilters = () => {
const {
suffix,
} = useLexicsStore()
const [selectedDate, setSelectedDate] = useState(new Date())
const [selectedMatchStatus, setSelectedMatchStatus] = useState<MatchStatuses>(MatchStatuses.Live)
const [selectedSportTypeId, setSelectedSportTypeId] = useState<SportTypes | null>(null)
const [selectedTournamentId, setSelectedTournamentId] = useState<number | null>(null)
const [selectedDate, setSelectedDate] = useQueryParamStore({
defaultValue: new Date(),
key: filterKeys.DATE,
validator: isValidDate,
})
const [
selectedMatchStatus,
setSelectedMatchStatus,
] = useQueryParamStore({
defaultValue: MatchStatuses.Live,
key: filterKeys.MATCH_STATUS,
validator: isValidMatchStatus,
})
const [
selectedSportTypeId,
setSelectedSportTypeId,
] = useQueryParamStore<SportTypes | null>({
defaultValue: null,
key: filterKeys.SPORT_TYPE,
validator: isValidSportType,
})
const [
selectedTournamentId,
setSelectedTournamentId,
] = useQueryParamStore<number | null>({
defaultValue: null,
key: filterKeys.TOURNAMENT_ID,
validator: isNumber,
})
const [teamId, setTeamId] = useState<number | null>(null)
const [matches, setMatches] = useState<Matches>({
broadcast: [],
@ -118,15 +155,13 @@ export const useFilters = () => {
fpOrderBy((match: Match) => Number(new Date(match.date)), 'desc'),
)(content) as Array<Match>, [suffix])
const preparedMatches = {
broadcast: prepareMatches(matches.broadcast),
features: prepareMatches(matches.features),
highlights: prepareMatches(matches.highlights),
isVideoSections: matches.isVideoSections,
}
const store = useMemo(() => ({
matches: preparedMatches,
matches: {
broadcast: prepareMatches(matches.broadcast),
features: prepareMatches(matches.features),
highlights: prepareMatches(matches.highlights),
isVideoSections: matches.isVideoSections,
},
selectedDate,
selectedMatchStatus,
selectedSportTypeId,
@ -141,9 +176,12 @@ export const useFilters = () => {
selectedMatchStatus,
selectedSportTypeId,
selectedTournamentId,
setSelectedDate,
setSelectedMatchStatus,
setSelectedSportTypeId,
setSelectedTournamentId,
setTeamId,
preparedMatches,
matches,
prepareMatches,
])
return store

@ -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)

@ -4,7 +4,11 @@ import React, {
useContext,
} from 'react'
import { useToggle } from 'hooks'
import isBoolean from 'lodash/isBoolean'
import { useLocalStore } from 'hooks'
const SCORE_KEY = 'hide_score'
type ScoreStore = {
isVisible: boolean,
@ -16,7 +20,12 @@ type Props = { children: ReactNode }
const ScoreContext = createContext({} as ScoreStore)
export const ScoreStore = ({ children }: Props) => {
const { isOpen, toggle } = useToggle()
const [isOpen, setIsOpen] = useLocalStore({
defaultValue: false,
key: SCORE_KEY,
validator: isBoolean,
})
const toggle = () => setIsOpen(!isOpen)
return (
<ScoreContext.Provider value={{ isVisible: isOpen, toggle }}>

@ -2,3 +2,4 @@ export * from './usePageId'
export * from './useToggle'
export * from './useRequest'
export * from './useSportNameParam'
export * from './useStorage'

@ -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)

@ -1,7 +1,7 @@
import { useState, useCallback } from 'react'
export const useToggle = () => {
const [isOpen, setIsOpen] = useState(false)
export const useToggle = (initialState = false) => {
const [isOpen, setIsOpen] = useState(initialState)
const open = useCallback(() => setIsOpen(true), [])
const close = useCallback(() => setIsOpen(false), [])
const toggle = useCallback(() => setIsOpen((state) => !state), [])

Loading…
Cancel
Save