Ott 530 extended search (#215)
* Ott 530 part 1/header refactoring (#211) * refactor(#530): moved Header into page components * refactor(#530): moved SportTypeFitler component into separte feature * refactor(#530): rever noUnusedLocals tsconfig * Ott 530 part 2/refactorings and filters (#212) * chore(#530): turned off props spreading rule * refactor(#530): moved mobile filters into home page * refactor(#530): moved RadioButtons into Commons * fix(#530): fixed ProfileLogo lazy image * refactor(#530): moved normalizer into helpers * fix(#530): no text when 2 radios on the page * feat(#530): header filter components * refactor(#530): turned off props spreading * Ott 530 part 3/store (#213) * refactor(#530): wip, request * refactor(#530): added link to search page * refactor(#530): prepare other components for reuse * feat(#530): added store * refactor(#530): fix pr comments * Ott 530 part 4/connect filters to store (#214) * refactor(#530): reading values from store * refactor(#530): added ExtendedSearchPage to App * fix(#530): removed score toggler in search pagekeep-around/af30b88d367751c9e05a735e4a0467a96238ef47
parent
ae799b6e66
commit
9b37149cb4
@ -0,0 +1,23 @@ |
|||||||
|
import React from 'react' |
||||||
|
|
||||||
|
import { PAGES } from 'config' |
||||||
|
|
||||||
|
import { Menu } from 'features/Menu' |
||||||
|
import { |
||||||
|
Wrapper, |
||||||
|
MenuWrapper, |
||||||
|
HomeButtonLink, |
||||||
|
} from 'features/ProfileHeader/styled' |
||||||
|
|
||||||
|
import { Filters } from '../Filters' |
||||||
|
|
||||||
|
export const DesktopHeader = () => ( |
||||||
|
<Wrapper> |
||||||
|
<MenuWrapper> |
||||||
|
<Menu /> |
||||||
|
<HomeButtonLink to={PAGES.home} /> |
||||||
|
</MenuWrapper> |
||||||
|
|
||||||
|
<Filters /> |
||||||
|
</Wrapper> |
||||||
|
) |
||||||
@ -0,0 +1,29 @@ |
|||||||
|
import React, { Fragment } from 'react' |
||||||
|
|
||||||
|
import { SportTypeFilter } from '../SportTypeFilter' |
||||||
|
import { SearchInput } from '../SearchInput' |
||||||
|
import { GenderFilter } from '../GenderFilter' |
||||||
|
import { ProfileFilter } from '../ProfileFilter' |
||||||
|
|
||||||
|
import { |
||||||
|
SportFilterWrapper, |
||||||
|
FilterWrapper, |
||||||
|
} from './styled' |
||||||
|
|
||||||
|
export const Filters = () => ( |
||||||
|
<Fragment> |
||||||
|
<FilterWrapper> |
||||||
|
<SearchInput /> |
||||||
|
</FilterWrapper> |
||||||
|
|
||||||
|
<SportFilterWrapper> |
||||||
|
<SportTypeFilter /> |
||||||
|
</SportFilterWrapper> |
||||||
|
<FilterWrapper> |
||||||
|
<GenderFilter /> |
||||||
|
</FilterWrapper> |
||||||
|
<FilterWrapper> |
||||||
|
<ProfileFilter /> |
||||||
|
</FilterWrapper> |
||||||
|
</Fragment> |
||||||
|
) |
||||||
@ -0,0 +1,43 @@ |
|||||||
|
import styled from 'styled-components/macro' |
||||||
|
|
||||||
|
import { devices } from 'config' |
||||||
|
|
||||||
|
export const BaseWrapper = styled.div` |
||||||
|
position: relative; |
||||||
|
height: 48px; |
||||||
|
margin-right: 16px; |
||||||
|
display: flex; |
||||||
|
` |
||||||
|
|
||||||
|
export const SportFilterWrapper = styled(BaseWrapper)` |
||||||
|
width: 173px; |
||||||
|
|
||||||
|
@media ${devices.tablet} { |
||||||
|
width: auto; |
||||||
|
margin: 20px 14px 0 14px; |
||||||
|
justify-content: center; |
||||||
|
} |
||||||
|
` |
||||||
|
|
||||||
|
export const FilterWrapper = styled(BaseWrapper)` |
||||||
|
width: auto; |
||||||
|
|
||||||
|
@media ${devices.tablet} { |
||||||
|
height: 30px; |
||||||
|
margin: 20px 20vw 0 20vw; |
||||||
|
justify-content: center; |
||||||
|
} |
||||||
|
` |
||||||
|
|
||||||
|
export const SearchInput = styled.input` |
||||||
|
width: 100%; |
||||||
|
padding-left: 20px; |
||||||
|
padding-right: 20px; |
||||||
|
font-weight: bold; |
||||||
|
font-size: 18px; |
||||||
|
background-color: #3F3F3F; |
||||||
|
color: #fff; |
||||||
|
border: transparent; |
||||||
|
border-color: transparent; |
||||||
|
outline: none; |
||||||
|
` |
||||||
@ -0,0 +1,36 @@ |
|||||||
|
import React from 'react' |
||||||
|
|
||||||
|
import { Gender } from 'requests' |
||||||
|
|
||||||
|
import { T9n } from 'features/T9n' |
||||||
|
import { |
||||||
|
RadioButtonGroup, |
||||||
|
RadioButton, |
||||||
|
} from 'features/Common' |
||||||
|
import { useExtendedSearchStore } from 'features/ExtendedSearchPage/store' |
||||||
|
|
||||||
|
export const GenderFilter = () => { |
||||||
|
const { |
||||||
|
onGenderChange, |
||||||
|
selectedGender, |
||||||
|
} = useExtendedSearchStore() |
||||||
|
|
||||||
|
return ( |
||||||
|
<RadioButtonGroup> |
||||||
|
<RadioButton |
||||||
|
selected={selectedGender === Gender.MALE} |
||||||
|
onClick={() => onGenderChange(Gender.MALE)} |
||||||
|
buttonWidth={122} |
||||||
|
> |
||||||
|
<T9n t='gender_male_long' /> |
||||||
|
</RadioButton> |
||||||
|
<RadioButton |
||||||
|
selected={selectedGender === Gender.FEMALE} |
||||||
|
onClick={() => onGenderChange(Gender.FEMALE)} |
||||||
|
buttonWidth={122} |
||||||
|
> |
||||||
|
<T9n t='gender_female_long' /> |
||||||
|
</RadioButton> |
||||||
|
</RadioButtonGroup> |
||||||
|
) |
||||||
|
} |
||||||
@ -0,0 +1,31 @@ |
|||||||
|
import React, { Fragment } from 'react' |
||||||
|
import { Link } from 'react-router-dom' |
||||||
|
|
||||||
|
import { PAGES } from 'config' |
||||||
|
|
||||||
|
import { Logo } from 'features/Logo' |
||||||
|
import { Menu } from 'features/Menu' |
||||||
|
|
||||||
|
import { |
||||||
|
HeaderMobileWrapper, |
||||||
|
HeaderIconsWrapper, |
||||||
|
IconFavWrapper, |
||||||
|
} from 'features/HeaderMobile/styled' |
||||||
|
|
||||||
|
import { Filters } from '../Filters' |
||||||
|
|
||||||
|
export const MobileHeader = () => ( |
||||||
|
<Fragment> |
||||||
|
<HeaderMobileWrapper> |
||||||
|
<Link to={PAGES.home}> |
||||||
|
<Logo width={52} height={12} /> |
||||||
|
</Link> |
||||||
|
<HeaderIconsWrapper> |
||||||
|
<IconFavWrapper /> |
||||||
|
<Menu /> |
||||||
|
</HeaderIconsWrapper> |
||||||
|
</HeaderMobileWrapper> |
||||||
|
|
||||||
|
<Filters /> |
||||||
|
</Fragment> |
||||||
|
) |
||||||
@ -0,0 +1,44 @@ |
|||||||
|
import React from 'react' |
||||||
|
|
||||||
|
import { ProfileTypes } from 'config' |
||||||
|
|
||||||
|
import { T9n } from 'features/T9n' |
||||||
|
import { |
||||||
|
RadioButtonGroup, |
||||||
|
RadioButton, |
||||||
|
} from 'features/Common' |
||||||
|
|
||||||
|
import { useExtendedSearchStore } from '../../store' |
||||||
|
|
||||||
|
export const ProfileFilter = () => { |
||||||
|
const { |
||||||
|
onProfileChange, |
||||||
|
selectedProfile, |
||||||
|
} = useExtendedSearchStore() |
||||||
|
|
||||||
|
return ( |
||||||
|
<RadioButtonGroup> |
||||||
|
<RadioButton |
||||||
|
selected={selectedProfile === ProfileTypes.TEAMS} |
||||||
|
onClick={() => onProfileChange(ProfileTypes.TEAMS)} |
||||||
|
buttonWidth={122} |
||||||
|
> |
||||||
|
<T9n t='team' /> |
||||||
|
</RadioButton> |
||||||
|
<RadioButton |
||||||
|
selected={selectedProfile === ProfileTypes.PLAYERS} |
||||||
|
onClick={() => onProfileChange(ProfileTypes.PLAYERS)} |
||||||
|
buttonWidth={122} |
||||||
|
> |
||||||
|
<T9n t='player' /> |
||||||
|
</RadioButton> |
||||||
|
<RadioButton |
||||||
|
selected={selectedProfile === ProfileTypes.TOURNAMENTS} |
||||||
|
onClick={() => onProfileChange(ProfileTypes.TOURNAMENTS)} |
||||||
|
buttonWidth={122} |
||||||
|
> |
||||||
|
<T9n t='tournament' /> |
||||||
|
</RadioButton> |
||||||
|
</RadioButtonGroup> |
||||||
|
) |
||||||
|
} |
||||||
@ -0,0 +1,47 @@ |
|||||||
|
import React, { |
||||||
|
Fragment, |
||||||
|
memo, |
||||||
|
} from 'react' |
||||||
|
|
||||||
|
import { isEmpty } from 'lodash' |
||||||
|
|
||||||
|
import { ProfileTypes } from 'config' |
||||||
|
|
||||||
|
import type { NormalizedSearchResults } from 'features/Search/helpers' |
||||||
|
|
||||||
|
import { |
||||||
|
Title, |
||||||
|
ResultsList, |
||||||
|
} from '../../styled' |
||||||
|
|
||||||
|
type Props = { |
||||||
|
results: NormalizedSearchResults, |
||||||
|
} |
||||||
|
|
||||||
|
export const Results = memo(({ results }: Props) => { |
||||||
|
const hasResults = ( |
||||||
|
!isEmpty(results.players) |
||||||
|
|| !isEmpty(results.teams) |
||||||
|
|| !isEmpty(results.tournaments) |
||||||
|
) |
||||||
|
if (hasResults) { |
||||||
|
return ( |
||||||
|
<Fragment> |
||||||
|
<Title t='search_results' /> |
||||||
|
<ResultsList |
||||||
|
list={results.teams} |
||||||
|
profileType={ProfileTypes.TEAMS} |
||||||
|
/> |
||||||
|
<ResultsList |
||||||
|
list={results.players} |
||||||
|
profileType={ProfileTypes.PLAYERS} |
||||||
|
/> |
||||||
|
<ResultsList |
||||||
|
list={results.tournaments} |
||||||
|
profileType={ProfileTypes.TOURNAMENTS} |
||||||
|
/> |
||||||
|
</Fragment> |
||||||
|
) |
||||||
|
} |
||||||
|
return null |
||||||
|
}) |
||||||
@ -0,0 +1,54 @@ |
|||||||
|
import React, { Fragment } from 'react' |
||||||
|
|
||||||
|
import styled from 'styled-components/macro' |
||||||
|
|
||||||
|
import { Loader } from 'features/Loader' |
||||||
|
import { useExtendedSearchStore } from 'features/ExtendedSearchPage/store' |
||||||
|
|
||||||
|
export const LoaderWrapper = styled.div` |
||||||
|
position: absolute; |
||||||
|
top: 0; |
||||||
|
background-color: rgba(129, 129, 129, 0.5); |
||||||
|
width: 100%; |
||||||
|
height: 100%; |
||||||
|
box-shadow: 0px 1px 1px rgba(0, 0, 0, 0.3); |
||||||
|
border-radius: 2px; |
||||||
|
display: flex; |
||||||
|
align-items: center; |
||||||
|
justify-content: center; |
||||||
|
` |
||||||
|
|
||||||
|
const Input = styled.input` |
||||||
|
width: 100%; |
||||||
|
padding-left: 20px; |
||||||
|
padding-right: 20px; |
||||||
|
font-weight: bold; |
||||||
|
font-size: 18px; |
||||||
|
background-color: #3F3F3F; |
||||||
|
color: #fff; |
||||||
|
border: transparent; |
||||||
|
border-color: transparent; |
||||||
|
outline: none; |
||||||
|
` |
||||||
|
|
||||||
|
export const SearchInput = () => { |
||||||
|
const { |
||||||
|
isFetching, |
||||||
|
onQueryChange, |
||||||
|
query, |
||||||
|
} = useExtendedSearchStore() |
||||||
|
return ( |
||||||
|
<Fragment> |
||||||
|
<Input |
||||||
|
role='search' |
||||||
|
value={query} |
||||||
|
onChange={onQueryChange} |
||||||
|
/> |
||||||
|
{isFetching && ( |
||||||
|
<LoaderWrapper> |
||||||
|
<Loader color='#515151' /> |
||||||
|
</LoaderWrapper> |
||||||
|
)} |
||||||
|
</Fragment> |
||||||
|
) |
||||||
|
} |
||||||
@ -0,0 +1,19 @@ |
|||||||
|
import React from 'react' |
||||||
|
|
||||||
|
import { SportTypeFilter as Filter } from 'features/SportTypeFilter' |
||||||
|
|
||||||
|
import { useExtendedSearchStore } from '../../store' |
||||||
|
|
||||||
|
export const SportTypeFilter = () => { |
||||||
|
const { |
||||||
|
onSportChange, |
||||||
|
selectedSport, |
||||||
|
} = useExtendedSearchStore() |
||||||
|
return ( |
||||||
|
<Filter |
||||||
|
selectedSportId={selectedSport} |
||||||
|
onSelect={onSportChange} |
||||||
|
onReset={() => onSportChange(null)} |
||||||
|
/> |
||||||
|
) |
||||||
|
} |
||||||
@ -0,0 +1,35 @@ |
|||||||
|
import React, { Fragment } from 'react' |
||||||
|
|
||||||
|
import { devices } from 'config' |
||||||
|
|
||||||
|
import { useMediaQuery } from 'features/MediaQuery' |
||||||
|
|
||||||
|
import { MobileHeader } from './components/MobileHeader' |
||||||
|
import { DesktopHeader } from './components/DesktopHeader' |
||||||
|
import { Results } from './components/Results' |
||||||
|
import { ExtendedSearchStore, useExtendedSearchStore } from './store' |
||||||
|
|
||||||
|
import { Main } from './styled' |
||||||
|
|
||||||
|
const ExtendedSearch = () => { |
||||||
|
const { searchItems } = useExtendedSearchStore() |
||||||
|
const isMobile = useMediaQuery({ query: devices.tablet }) |
||||||
|
return ( |
||||||
|
<Fragment> |
||||||
|
{ |
||||||
|
isMobile |
||||||
|
? <MobileHeader /> |
||||||
|
: <DesktopHeader /> |
||||||
|
} |
||||||
|
<Main> |
||||||
|
<Results results={searchItems} /> |
||||||
|
</Main> |
||||||
|
</Fragment> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
export const ExtendedSearchPage = () => ( |
||||||
|
<ExtendedSearchStore> |
||||||
|
<ExtendedSearch /> |
||||||
|
</ExtendedSearchStore> |
||||||
|
) |
||||||
@ -0,0 +1,87 @@ |
|||||||
|
import type { ChangeEvent } from 'react' |
||||||
|
import { |
||||||
|
useCallback, |
||||||
|
useEffect, |
||||||
|
useState, |
||||||
|
} from 'react' |
||||||
|
|
||||||
|
import trim from 'lodash/trim' |
||||||
|
import size from 'lodash/size' |
||||||
|
|
||||||
|
import { ProfileTypes, SportTypes } from 'config' |
||||||
|
|
||||||
|
import { Gender } from 'requests' |
||||||
|
|
||||||
|
import { MIN_CHARACTERS_LENGTH } from 'features/Search/config' |
||||||
|
import type { NormalizedSearchResults } from 'features/Search/helpers' |
||||||
|
|
||||||
|
import { useSearchRequest } from './useSearchRequest' |
||||||
|
|
||||||
|
const initialState = { |
||||||
|
players: [], |
||||||
|
teams: [], |
||||||
|
tournaments: [], |
||||||
|
} |
||||||
|
|
||||||
|
export const useExtendedSearch = () => { |
||||||
|
const [searchItems, setSearchItems] = useState<NormalizedSearchResults>(initialState) |
||||||
|
|
||||||
|
const [query, setQuery] = useState('') |
||||||
|
const [selectedSport, setSelectedSport] = useState<SportTypes | null>(null) |
||||||
|
const [selectedGender, setSelectedGender] = useState<Gender | null>(null) |
||||||
|
const [selectedProfile, setSelectedProfile] = useState<ProfileTypes | null>(null) |
||||||
|
|
||||||
|
const { |
||||||
|
cancelSearch, |
||||||
|
isFetching, |
||||||
|
search, |
||||||
|
} = useSearchRequest(setSearchItems) |
||||||
|
|
||||||
|
const onQueryChange = useCallback( |
||||||
|
({ target: { value } }: ChangeEvent<HTMLInputElement>) => { |
||||||
|
setQuery(value) |
||||||
|
}, |
||||||
|
[], |
||||||
|
) |
||||||
|
|
||||||
|
const onGenderChange = useCallback((gender: Gender | null) => { |
||||||
|
setSelectedGender((oldGender) => (gender === oldGender ? null : gender)) |
||||||
|
}, []) |
||||||
|
|
||||||
|
const onProfileChange = useCallback((profile: ProfileTypes | null) => { |
||||||
|
setSelectedProfile((oldProfile) => (profile === oldProfile ? null : profile)) |
||||||
|
}, []) |
||||||
|
|
||||||
|
const trimmedQuery = trim(query) |
||||||
|
useEffect(() => { |
||||||
|
cancelSearch() |
||||||
|
if (size(trimmedQuery) >= MIN_CHARACTERS_LENGTH) { |
||||||
|
search({ |
||||||
|
gender: selectedGender, |
||||||
|
profileType: selectedProfile, |
||||||
|
query: trimmedQuery, |
||||||
|
sportType: selectedSport, |
||||||
|
}) |
||||||
|
} |
||||||
|
}, [ |
||||||
|
trimmedQuery, |
||||||
|
selectedSport, |
||||||
|
selectedGender, |
||||||
|
selectedProfile, |
||||||
|
search, |
||||||
|
cancelSearch, |
||||||
|
]) |
||||||
|
|
||||||
|
return { |
||||||
|
isFetching, |
||||||
|
onGenderChange, |
||||||
|
onProfileChange, |
||||||
|
onQueryChange, |
||||||
|
onSportChange: setSelectedSport, |
||||||
|
query, |
||||||
|
searchItems, |
||||||
|
selectedGender, |
||||||
|
selectedProfile, |
||||||
|
selectedSport, |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,53 @@ |
|||||||
|
import { |
||||||
|
useCallback, |
||||||
|
useMemo, |
||||||
|
useRef, |
||||||
|
} from 'react' |
||||||
|
|
||||||
|
import debounce from 'lodash/debounce' |
||||||
|
|
||||||
|
import { extendedSearch } from 'requests' |
||||||
|
|
||||||
|
import { useRequest } from 'hooks' |
||||||
|
|
||||||
|
import { SEARCH_DELAY } from 'features/Search/config' |
||||||
|
import type { NormalizedSearchResults } from 'features/Search/helpers' |
||||||
|
import { normalizeItems } from 'features/Search/helpers' |
||||||
|
|
||||||
|
type Updater = (results: NormalizedSearchResults) => void |
||||||
|
type SearchArgs = Omit<Parameters<typeof extendedSearch>[0], 'abortSignal'> |
||||||
|
|
||||||
|
export const useSearchRequest = (updater: Updater) => { |
||||||
|
const abortRef = useRef<AbortController | null>(null) |
||||||
|
|
||||||
|
const { |
||||||
|
isFetching, |
||||||
|
request: searchItemsRequest, |
||||||
|
} = useRequest(extendedSearch) |
||||||
|
|
||||||
|
const search = useMemo( |
||||||
|
() => debounce((args: SearchArgs) => { |
||||||
|
const abortController = new AbortController() |
||||||
|
searchItemsRequest({ |
||||||
|
abortSignal: abortController.signal, |
||||||
|
...args, |
||||||
|
}).then((searchResult) => { |
||||||
|
abortRef.current = null |
||||||
|
updater(normalizeItems(searchResult)) |
||||||
|
}) |
||||||
|
abortRef.current = abortController |
||||||
|
}, SEARCH_DELAY), |
||||||
|
[searchItemsRequest, updater], |
||||||
|
) |
||||||
|
|
||||||
|
const cancelSearch = useCallback(() => { |
||||||
|
abortRef.current?.abort() |
||||||
|
abortRef.current = null |
||||||
|
}, []) |
||||||
|
|
||||||
|
return { |
||||||
|
cancelSearch, |
||||||
|
isFetching, |
||||||
|
search, |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,19 @@ |
|||||||
|
import type { ReactNode } from 'react' |
||||||
|
import React, { |
||||||
|
createContext, |
||||||
|
useContext, |
||||||
|
} from 'react' |
||||||
|
|
||||||
|
import { useExtendedSearch } from './hooks' |
||||||
|
|
||||||
|
type Context = ReturnType<typeof useExtendedSearch> |
||||||
|
type Props = { children: ReactNode } |
||||||
|
|
||||||
|
const SearchContext = createContext({} as Context) |
||||||
|
|
||||||
|
export const ExtendedSearchStore = ({ children }: Props) => { |
||||||
|
const value = useExtendedSearch() |
||||||
|
return <SearchContext.Provider value={value}>{children}</SearchContext.Provider> |
||||||
|
} |
||||||
|
|
||||||
|
export const useExtendedSearchStore = () => useContext(SearchContext) |
||||||
@ -0,0 +1,41 @@ |
|||||||
|
import styled from 'styled-components/macro' |
||||||
|
|
||||||
|
import { devices } from 'config' |
||||||
|
|
||||||
|
import { T9n } from 'features/T9n' |
||||||
|
import { ItemsList } from 'features/ItemsList' |
||||||
|
import { GenderComponent, StyledLink } from 'features/ItemsList/styled' |
||||||
|
|
||||||
|
export const Main = styled.main` |
||||||
|
margin-top: 75px; |
||||||
|
margin-bottom: 30px; |
||||||
|
|
||||||
|
@media ${devices.tablet} { |
||||||
|
margin-top: 30px; |
||||||
|
} |
||||||
|
` |
||||||
|
|
||||||
|
export const Title = styled(T9n)` |
||||||
|
display: block; |
||||||
|
font-weight: bold; |
||||||
|
font-size: 36px; |
||||||
|
line-height: 24px; |
||||||
|
color: #fff; |
||||||
|
margin-bottom: 30px; |
||||||
|
padding: 0 14px; |
||||||
|
` |
||||||
|
|
||||||
|
export const ResultsList = styled(ItemsList)` |
||||||
|
${StyledLink} { |
||||||
|
background-color: transparent; |
||||||
|
|
||||||
|
:focus-within, |
||||||
|
:hover { |
||||||
|
background-color: rgba(153, 153, 153, 0.4); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
${GenderComponent} { |
||||||
|
display: none; |
||||||
|
} |
||||||
|
` |
||||||
@ -1,62 +0,0 @@ |
|||||||
import React, { Fragment } from 'react' |
|
||||||
import { Route, Switch } from 'react-router-dom' |
|
||||||
|
|
||||||
import { Menu } from 'features/Menu' |
|
||||||
import { Search } from 'features/Search' |
|
||||||
|
|
||||||
import { |
|
||||||
DateFilter, |
|
||||||
MatchStatusFilter, |
|
||||||
SportTypeFilter, |
|
||||||
TournamentFilter, |
|
||||||
} from 'features/HeaderFilters' |
|
||||||
|
|
||||||
import { PAGES } from 'config' |
|
||||||
|
|
||||||
import { |
|
||||||
Wrapper, |
|
||||||
FilterWrapper, |
|
||||||
HomeButtonLink, |
|
||||||
SearchWrapper, |
|
||||||
SportFilterWrapper, |
|
||||||
MenuWrapper, |
|
||||||
} from './styled' |
|
||||||
|
|
||||||
export const Header = () => ( |
|
||||||
<Wrapper> |
|
||||||
<Switch> |
|
||||||
<Route path={PAGES.home} exact> |
|
||||||
<MenuWrapper> |
|
||||||
<Menu /> |
|
||||||
</MenuWrapper> |
|
||||||
|
|
||||||
<SearchWrapper> |
|
||||||
<Search /> |
|
||||||
</SearchWrapper> |
|
||||||
|
|
||||||
<FilterWrapper> |
|
||||||
<DateFilter /> |
|
||||||
</FilterWrapper> |
|
||||||
|
|
||||||
<FilterWrapper> |
|
||||||
<MatchStatusFilter /> |
|
||||||
</FilterWrapper> |
|
||||||
|
|
||||||
<SportFilterWrapper> |
|
||||||
<SportTypeFilter /> |
|
||||||
<TournamentFilter /> |
|
||||||
</SportFilterWrapper> |
|
||||||
</Route> |
|
||||||
|
|
||||||
<Fragment> |
|
||||||
<MenuWrapper> |
|
||||||
<Menu /> |
|
||||||
<HomeButtonLink to={PAGES.home} /> |
|
||||||
</MenuWrapper> |
|
||||||
<SearchWrapper> |
|
||||||
<Search /> |
|
||||||
</SearchWrapper> |
|
||||||
</Fragment> |
|
||||||
</Switch> |
|
||||||
</Wrapper> |
|
||||||
) |
|
||||||
@ -1,72 +1,65 @@ |
|||||||
import React from 'react' |
import React from 'react' |
||||||
|
|
||||||
import map from 'lodash/map' |
import { css } from 'styled-components/macro' |
||||||
|
|
||||||
import { T9n } from 'features/T9n' |
import { devices } from 'config' |
||||||
import { OutsideClick } from 'features/OutsideClick' |
|
||||||
|
|
||||||
import { useSportTypeFilter } from './hooks' |
import { useHeaderFiltersStore } from 'features/HeaderFilters/store' |
||||||
import { |
import { SportTypeFilter as Filter } from 'features/SportTypeFilter' |
||||||
Wrapper, |
|
||||||
SportList, |
|
||||||
CustomOption, |
|
||||||
} from './styled' |
|
||||||
import { |
|
||||||
DropdownButton, |
|
||||||
ButtonTitle, |
|
||||||
ClearButton, |
|
||||||
Arrows, |
|
||||||
} from '../TournamentFilter/styled' |
|
||||||
|
|
||||||
export const SportTypeFilter = () => { |
import { DropdownButton } from '../TournamentFilter/styled' |
||||||
|
|
||||||
|
const wrapperStyles = css` |
||||||
|
width: 50%; |
||||||
|
border-right: 1px solid #222222; |
||||||
|
|
||||||
|
@media ${devices.tablet} { |
||||||
|
border-right: 0; |
||||||
|
} |
||||||
|
|
||||||
|
${DropdownButton} { |
||||||
|
border-radius: 0; |
||||||
|
border-top-left-radius: 2px; |
||||||
|
border-bottom-left-radius: 2px; |
||||||
|
} |
||||||
|
` |
||||||
|
|
||||||
|
const useSportTypeFilter = () => { |
||||||
const { |
const { |
||||||
close, |
selectedSportTypeId, |
||||||
isOpen, |
setSelectedSportTypeId, |
||||||
onResetSelectedSport, |
setSelectedTournamentId, |
||||||
onSelect, |
} = useHeaderFiltersStore() |
||||||
open, |
|
||||||
selectedSportType, |
|
||||||
sportList, |
|
||||||
} = useSportTypeFilter() |
|
||||||
|
|
||||||
return ( |
const onSelect = (id: number) => { |
||||||
<Wrapper> |
setSelectedSportTypeId(id) |
||||||
<DropdownButton |
setSelectedTournamentId(null) |
||||||
active={isOpen} |
} |
||||||
onClick={open} |
|
||||||
aria-expanded={isOpen} |
|
||||||
aria-controls='sportList' |
|
||||||
> |
|
||||||
<ButtonTitle> |
|
||||||
<T9n t={selectedSportType?.lexic || 'sport'} /> |
|
||||||
</ButtonTitle> |
|
||||||
{selectedSportType && <ClearButton onClick={onResetSelectedSport} />} |
|
||||||
<Arrows active={isOpen} /> |
|
||||||
</DropdownButton> |
|
||||||
|
|
||||||
{ |
const onReset = () => { |
||||||
isOpen && ( |
setSelectedSportTypeId(null) |
||||||
<OutsideClick onClick={close}> |
setSelectedTournamentId(null) |
||||||
<SportList id='sportList' role='listbox'> |
|
||||||
{ |
|
||||||
map(sportList, ({ |
|
||||||
id, |
|
||||||
lexic, |
|
||||||
}) => ( |
|
||||||
<CustomOption |
|
||||||
key={id} |
|
||||||
sport={id} |
|
||||||
onClick={() => onSelect(id)} |
|
||||||
role='option' |
|
||||||
> |
|
||||||
<T9n t={lexic} /> |
|
||||||
</CustomOption> |
|
||||||
)) |
|
||||||
} |
} |
||||||
</SportList> |
|
||||||
</OutsideClick> |
return { |
||||||
) |
onReset, |
||||||
|
onSelect, |
||||||
|
selectedSportTypeId, |
||||||
} |
} |
||||||
</Wrapper> |
} |
||||||
|
|
||||||
|
export const SportTypeFilter = () => { |
||||||
|
const { |
||||||
|
onReset, |
||||||
|
onSelect, |
||||||
|
selectedSportTypeId, |
||||||
|
} = useSportTypeFilter() |
||||||
|
return ( |
||||||
|
<Filter |
||||||
|
selectedSportId={selectedSportTypeId} |
||||||
|
onSelect={onSelect} |
||||||
|
onReset={onReset} |
||||||
|
wrapperStyles={wrapperStyles} |
||||||
|
/> |
||||||
) |
) |
||||||
} |
} |
||||||
|
|||||||
@ -0,0 +1,44 @@ |
|||||||
|
import React from 'react' |
||||||
|
|
||||||
|
import { Menu } from 'features/Menu' |
||||||
|
import { Search } from 'features/Search' |
||||||
|
|
||||||
|
import { |
||||||
|
DateFilter, |
||||||
|
MatchStatusFilter, |
||||||
|
SportTypeFilter, |
||||||
|
TournamentFilter, |
||||||
|
} from 'features/HeaderFilters' |
||||||
|
|
||||||
|
import { |
||||||
|
Wrapper, |
||||||
|
FilterWrapper, |
||||||
|
SearchWrapper, |
||||||
|
SportFilterWrapper, |
||||||
|
MenuWrapper, |
||||||
|
} from 'features/ProfileHeader/styled' |
||||||
|
|
||||||
|
export const Header = () => ( |
||||||
|
<Wrapper> |
||||||
|
<MenuWrapper> |
||||||
|
<Menu /> |
||||||
|
</MenuWrapper> |
||||||
|
|
||||||
|
<SearchWrapper> |
||||||
|
<Search /> |
||||||
|
</SearchWrapper> |
||||||
|
|
||||||
|
<FilterWrapper> |
||||||
|
<DateFilter /> |
||||||
|
</FilterWrapper> |
||||||
|
|
||||||
|
<FilterWrapper> |
||||||
|
<MatchStatusFilter /> |
||||||
|
</FilterWrapper> |
||||||
|
|
||||||
|
<SportFilterWrapper> |
||||||
|
<SportTypeFilter /> |
||||||
|
<TournamentFilter /> |
||||||
|
</SportFilterWrapper> |
||||||
|
</Wrapper> |
||||||
|
) |
||||||
@ -0,0 +1,35 @@ |
|||||||
|
import React from 'react' |
||||||
|
|
||||||
|
import { devices, PAGES } from 'config' |
||||||
|
|
||||||
|
import { Menu } from 'features/Menu' |
||||||
|
import { Search } from 'features/Search' |
||||||
|
import { HeaderMobile } from 'features/HeaderMobile' |
||||||
|
import { useMediaQuery } from 'features/MediaQuery' |
||||||
|
|
||||||
|
import { |
||||||
|
Wrapper, |
||||||
|
HomeButtonLink, |
||||||
|
SearchWrapper, |
||||||
|
MenuWrapper, |
||||||
|
} from './styled' |
||||||
|
|
||||||
|
export const ProfileHeader = () => { |
||||||
|
const isMobile = useMediaQuery({ query: devices.tablet }) |
||||||
|
|
||||||
|
return ( |
||||||
|
isMobile |
||||||
|
? <HeaderMobile /> |
||||||
|
: ( |
||||||
|
<Wrapper> |
||||||
|
<MenuWrapper> |
||||||
|
<Menu /> |
||||||
|
<HomeButtonLink to={PAGES.home} /> |
||||||
|
</MenuWrapper> |
||||||
|
<SearchWrapper> |
||||||
|
<Search /> |
||||||
|
</SearchWrapper> |
||||||
|
</Wrapper> |
||||||
|
) |
||||||
|
) |
||||||
|
} |
||||||
@ -1,6 +1,8 @@ |
|||||||
import map from 'lodash/map' |
import map from 'lodash/map' |
||||||
|
|
||||||
import { SearchItems } from 'requests' |
import type { SearchItems } from 'requests' |
||||||
|
|
||||||
|
export type NormalizedSearchResults = ReturnType<typeof normalizeItems> |
||||||
|
|
||||||
export const normalizeItems = (searchItems: SearchItems) => { |
export const normalizeItems = (searchItems: SearchItems) => { |
||||||
const players = map(searchItems.players, (player) => ({ |
const players = map(searchItems.players, (player) => ({ |
||||||
@ -0,0 +1,74 @@ |
|||||||
|
import React from 'react' |
||||||
|
|
||||||
|
import map from 'lodash/map' |
||||||
|
|
||||||
|
import { T9n } from 'features/T9n' |
||||||
|
import { OutsideClick } from 'features/OutsideClick' |
||||||
|
|
||||||
|
import type { Props } from './hooks' |
||||||
|
import { useSportTypeFilter } from './hooks' |
||||||
|
import { |
||||||
|
Wrapper, |
||||||
|
SportList, |
||||||
|
SportItem, |
||||||
|
} from './styled' |
||||||
|
import { |
||||||
|
DropdownButton, |
||||||
|
ButtonTitle, |
||||||
|
ClearButton, |
||||||
|
Arrows, |
||||||
|
} from '../HeaderFilters/components/TournamentFilter/styled' |
||||||
|
|
||||||
|
export const SportTypeFilter = (props: Props) => { |
||||||
|
const { wrapperStyles } = props |
||||||
|
const { |
||||||
|
close, |
||||||
|
isOpen, |
||||||
|
onResetSelectedSport, |
||||||
|
onSelect, |
||||||
|
open, |
||||||
|
selectedSportType, |
||||||
|
sportList, |
||||||
|
} = useSportTypeFilter(props) |
||||||
|
|
||||||
|
return ( |
||||||
|
<Wrapper customstyles={wrapperStyles}> |
||||||
|
<DropdownButton |
||||||
|
active={isOpen} |
||||||
|
onClick={open} |
||||||
|
aria-expanded={isOpen} |
||||||
|
aria-controls='sportList' |
||||||
|
> |
||||||
|
<ButtonTitle> |
||||||
|
<T9n t={selectedSportType?.lexic || 'sport'} /> |
||||||
|
</ButtonTitle> |
||||||
|
{selectedSportType && <ClearButton onClick={onResetSelectedSport} />} |
||||||
|
<Arrows active={isOpen} /> |
||||||
|
</DropdownButton> |
||||||
|
|
||||||
|
{ |
||||||
|
isOpen && ( |
||||||
|
<OutsideClick onClick={close}> |
||||||
|
<SportList id='sportList' role='listbox'> |
||||||
|
{ |
||||||
|
map(sportList, ({ |
||||||
|
id, |
||||||
|
lexic, |
||||||
|
}) => ( |
||||||
|
<SportItem |
||||||
|
key={id} |
||||||
|
sport={id} |
||||||
|
onClick={() => onSelect(id)} |
||||||
|
role='option' |
||||||
|
> |
||||||
|
<T9n t={lexic} /> |
||||||
|
</SportItem> |
||||||
|
)) |
||||||
|
} |
||||||
|
</SportList> |
||||||
|
</OutsideClick> |
||||||
|
) |
||||||
|
} |
||||||
|
</Wrapper> |
||||||
|
) |
||||||
|
} |
||||||
@ -0,0 +1,81 @@ |
|||||||
|
import isNull from 'lodash/isNull' |
||||||
|
import filter from 'lodash/filter' |
||||||
|
|
||||||
|
import { |
||||||
|
ProfileTypes, |
||||||
|
SportTypes, |
||||||
|
PROFILE_NAMES, |
||||||
|
} from 'config' |
||||||
|
|
||||||
|
import { |
||||||
|
Gender, |
||||||
|
getSearchItems, |
||||||
|
SearchItems, |
||||||
|
} from './getSearchItems' |
||||||
|
|
||||||
|
type FilterArgs = { |
||||||
|
gender: Gender | null, |
||||||
|
profileType: ProfileTypes | null, |
||||||
|
results: SearchItems, |
||||||
|
sportType: SportTypes | null, |
||||||
|
} |
||||||
|
|
||||||
|
type Item = { |
||||||
|
gender?: Gender, |
||||||
|
sport: SportTypes, |
||||||
|
} |
||||||
|
|
||||||
|
// этот хелпер и его типы удалим когда сделают
|
||||||
|
// норм процу с фильтрацией по полу, профилю и спорту
|
||||||
|
const filterResults = ({ |
||||||
|
gender, |
||||||
|
profileType, |
||||||
|
results, |
||||||
|
sportType, |
||||||
|
}: FilterArgs) => { |
||||||
|
const filterFunc = (item: Item) => { |
||||||
|
let result = true |
||||||
|
if (!isNull(gender)) { |
||||||
|
result = item.gender === gender |
||||||
|
} |
||||||
|
if (!isNull(sportType)) { |
||||||
|
result = item.sport === sportType |
||||||
|
} |
||||||
|
return result |
||||||
|
} |
||||||
|
|
||||||
|
const filteredResults = { |
||||||
|
players: filter(results.players, filterFunc), |
||||||
|
teams: filter(results.teams, filterFunc), |
||||||
|
tournaments: filter(results.tournaments, filterFunc), |
||||||
|
} |
||||||
|
|
||||||
|
if (isNull(profileType)) return filteredResults |
||||||
|
|
||||||
|
const key = PROFILE_NAMES[profileType] |
||||||
|
return { [key]: filteredResults[key] } |
||||||
|
} |
||||||
|
|
||||||
|
type Args = { |
||||||
|
abortSignal: AbortSignal, |
||||||
|
gender: Gender | null, |
||||||
|
profileType: ProfileTypes | null, |
||||||
|
query: string, |
||||||
|
sportType: SportTypes | null, |
||||||
|
} |
||||||
|
|
||||||
|
export const extendedSearch = async ({ |
||||||
|
abortSignal, |
||||||
|
gender, |
||||||
|
profileType, |
||||||
|
query, |
||||||
|
sportType, |
||||||
|
}: Args) => { |
||||||
|
const results = await getSearchItems(query, abortSignal) |
||||||
|
return filterResults({ |
||||||
|
gender, |
||||||
|
profileType, |
||||||
|
results, |
||||||
|
sportType, |
||||||
|
}) |
||||||
|
} |
||||||
Loading…
Reference in new issue