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 map from 'lodash/map' |
||||
import { css } from 'styled-components/macro' |
||||
|
||||
import { T9n } from 'features/T9n' |
||||
import { OutsideClick } from 'features/OutsideClick' |
||||
import { devices } from 'config' |
||||
|
||||
import { useSportTypeFilter } from './hooks' |
||||
import { |
||||
Wrapper, |
||||
SportList, |
||||
CustomOption, |
||||
} from './styled' |
||||
import { |
||||
DropdownButton, |
||||
ButtonTitle, |
||||
ClearButton, |
||||
Arrows, |
||||
} from '../TournamentFilter/styled' |
||||
import { useHeaderFiltersStore } from 'features/HeaderFilters/store' |
||||
import { SportTypeFilter as Filter } from 'features/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 { |
||||
selectedSportTypeId, |
||||
setSelectedSportTypeId, |
||||
setSelectedTournamentId, |
||||
} = useHeaderFiltersStore() |
||||
|
||||
const onSelect = (id: number) => { |
||||
setSelectedSportTypeId(id) |
||||
setSelectedTournamentId(null) |
||||
} |
||||
|
||||
const onReset = () => { |
||||
setSelectedSportTypeId(null) |
||||
setSelectedTournamentId(null) |
||||
} |
||||
|
||||
return { |
||||
onReset, |
||||
onSelect, |
||||
selectedSportTypeId, |
||||
} |
||||
} |
||||
|
||||
export const SportTypeFilter = () => { |
||||
const { |
||||
close, |
||||
isOpen, |
||||
onResetSelectedSport, |
||||
onReset, |
||||
onSelect, |
||||
open, |
||||
selectedSportType, |
||||
sportList, |
||||
selectedSportTypeId, |
||||
} = useSportTypeFilter() |
||||
|
||||
return ( |
||||
<Wrapper> |
||||
<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, |
||||
}) => ( |
||||
<CustomOption |
||||
key={id} |
||||
sport={id} |
||||
onClick={() => onSelect(id)} |
||||
role='option' |
||||
> |
||||
<T9n t={lexic} /> |
||||
</CustomOption> |
||||
)) |
||||
} |
||||
</SportList> |
||||
</OutsideClick> |
||||
) |
||||
} |
||||
</Wrapper> |
||||
<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 { SearchItems } from 'requests' |
||||
import type { SearchItems } from 'requests' |
||||
|
||||
export type NormalizedSearchResults = ReturnType<typeof normalizeItems> |
||||
|
||||
export const normalizeItems = (searchItems: SearchItems) => { |
||||
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