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 page
keep-around/af30b88d367751c9e05a735e4a0467a96238ef47
Mirlan 5 years ago committed by GitHub
parent ae799b6e66
commit 9b37149cb4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      src/config/lexics/indexLexics.tsx
  2. 1
      src/config/pages.tsx
  3. 84
      src/features/App/AuthenticatedApp.tsx
  4. 16
      src/features/Common/RadioButtons/index.tsx
  5. 1
      src/features/Common/index.tsx
  6. 23
      src/features/ExtendedSearchPage/components/DesktopHeader/index.tsx
  7. 29
      src/features/ExtendedSearchPage/components/Filters/index.tsx
  8. 43
      src/features/ExtendedSearchPage/components/Filters/styled.tsx
  9. 36
      src/features/ExtendedSearchPage/components/GenderFilter/index.tsx
  10. 31
      src/features/ExtendedSearchPage/components/MobileHeader/index.tsx
  11. 44
      src/features/ExtendedSearchPage/components/ProfileFilter/index.tsx
  12. 47
      src/features/ExtendedSearchPage/components/Results/index.tsx
  13. 54
      src/features/ExtendedSearchPage/components/SearchInput/index.tsx
  14. 19
      src/features/ExtendedSearchPage/components/SportTypeFilter/index.tsx
  15. 35
      src/features/ExtendedSearchPage/index.tsx
  16. 87
      src/features/ExtendedSearchPage/store/hooks/index.tsx
  17. 53
      src/features/ExtendedSearchPage/store/hooks/useSearchRequest.tsx
  18. 19
      src/features/ExtendedSearchPage/store/index.tsx
  19. 41
      src/features/ExtendedSearchPage/styled.tsx
  20. 62
      src/features/Header/index.tsx
  21. 7
      src/features/HeaderFilters/components/MatchStatusFilter/index.tsx
  22. 113
      src/features/HeaderFilters/components/SportTypeFilter/index.tsx
  23. 2
      src/features/HeaderFilters/components/TournamentFilter/styled.tsx
  24. 44
      src/features/HomePage/components/Header/index.tsx
  25. 47
      src/features/HomePage/index.tsx
  26. 6
      src/features/ItemsList/index.tsx
  27. 56
      src/features/MatchPage/index.tsx
  28. 25
      src/features/PlayerPage/index.tsx
  29. 35
      src/features/ProfileHeader/index.tsx
  30. 7
      src/features/ProfileHeader/styled.tsx
  31. 2
      src/features/ProfileLogo/index.tsx
  32. 4
      src/features/Search/helpers.tsx
  33. 14
      src/features/Search/hooks/index.tsx
  34. 75
      src/features/Search/index.tsx
  35. 47
      src/features/Search/styled.tsx
  36. 36
      src/features/SportTypeFilter/hooks.tsx
  37. 74
      src/features/SportTypeFilter/index.tsx
  38. 18
      src/features/SportTypeFilter/styled.tsx
  39. 2
      src/features/TeamPage/index.tsx
  40. 2
      src/features/TournamentPage/index.tsx
  41. 1
      src/requests/index.tsx
  42. 81
      src/requests/search.tsx

@ -13,7 +13,10 @@ export const indexLexics = {
game_finished: 13026,
game_time: 13029,
gender_female: 9648,
gender_female_long: 13374,
gender_male: 9647,
gender_male_long: 13373,
go_to_extended_search_page: 13375,
goals: 13030,
hide_score: 12982,
highlights: 13033,
@ -30,6 +33,7 @@ export const indexLexics = {
player: 630,
players_video: 13032,
round_highilights: 13050,
search_results: 9014,
select_language: 1005,
sport: 12993,
team: 658,

@ -1,4 +1,5 @@
export const PAGES = {
extendedSearch: '/search',
home: '/',
login: '/login',
match: '/matches',

@ -1,4 +1,4 @@
import React, { Fragment } from 'react'
import React from 'react'
import {
Route,
Redirect,
@ -15,88 +15,60 @@ import { TeamPage } from 'features/TeamPage'
import { MatchPage } from 'features/MatchPage'
import { PlayerPage } from 'features/PlayerPage'
import { TournamentPage } from 'features/TournamentPage'
import { ExtendedSearchPage } from 'features/ExtendedSearchPage'
import { LanguageSelect } from 'features/LanguageSelect'
import { UserAccount } from 'features/UserAccount'
import { ScoreStore, ToggleScore } from 'features/ToggleScore'
import { Header } from 'features/Header'
import { SportFilterWrapper } from 'features/Header/styled'
import { MainWrapper } from 'features/MainWrapper'
import {
HeaderFiltersStore,
TournamentFilter,
SportTypeFilter,
} from 'features/HeaderFilters'
import { UserFavorites } from 'features/UserFavorites'
import { UserFavoritesStore } from 'features/UserFavorites/store'
import { useMediaQuery } from 'features/MediaQuery'
import { HeaderMobile } from 'features/HeaderMobile'
import { FormStore } from 'features/FormStore'
export const AuthenticatedApp = () => {
useLexicsConfig(indexLexics)
const isMobile = useMediaQuery({ query: devices.tablet })
const isUserAccountPage = useRouteMatch(PAGES.useraccount)?.isExact || false
const isExtendedSearchPage = useRouteMatch(PAGES.extendedSearch)?.isExact || false
return (
<ScoreStore>
{
isMobile || isUserAccountPage
? null
: (
<Fragment>
<ToggleScore />
<LanguageSelect />
</Fragment>
)
: <LanguageSelect />
}
{isExtendedSearchPage ? null : <ToggleScore />}
<Switch>
<Route path={PAGES.useraccount}>
<UserAccountForm />
</Route>
<HeaderFiltersStore>
<UserFavoritesStore>
<MainWrapper>
{
isMobile
? <HeaderMobile />
: (
<Fragment>
<UserFavorites />
<Header />
</Fragment>
)
}
<Route exact path={PAGES.home}>
<HomePage />
</Route>
<UserFavoritesStore>
<MainWrapper>
{!isMobile && <UserFavorites />}
<Route exact path={PAGES.home}>
<HomePage />
</Route>
<Route path={`/:sportName${PAGES.tournament}/:pageId`}>
<TournamentPage />
</Route>
<Route path={`/:sportName${PAGES.tournament}/:pageId`}>
<TournamentPage />
</Route>
<Route path={`/:sportName${PAGES.team}/:pageId`}>
<TeamPage />
</Route>
<Route path={`/:sportName${PAGES.player}/:pageId`}>
<PlayerPage />
</Route>
<Route path={`/:sportName${PAGES.match}/:pageId`}>
<MatchPage />
</Route>
{
isMobile
? (
<SportFilterWrapper>
<SportTypeFilter />
<TournamentFilter />
</SportFilterWrapper>
)
: null
}
</MainWrapper>
</UserFavoritesStore>
</HeaderFiltersStore>
<Route path={`/:sportName${PAGES.team}/:pageId`}>
<TeamPage />
</Route>
<Route path={`/:sportName${PAGES.player}/:pageId`}>
<PlayerPage />
</Route>
<Route path={`/:sportName${PAGES.match}/:pageId`}>
<MatchPage />
</Route>
<Route path={PAGES.extendedSearch}>
<ExtendedSearchPage />
</Route>
</MainWrapper>
</UserFavoritesStore>
<Redirect to={PAGES.home} />
</Switch>
</ScoreStore>

@ -1,4 +1,4 @@
import styled from 'styled-components/macro'
import styled, { css } from 'styled-components/macro'
import { devices } from 'config/devices'
@ -14,6 +14,7 @@ export const RadioButtonGroup = styled.div.attrs({
type RadioButtonProps = {
buttonWidth?: number,
selected?: boolean,
upperCase?: boolean,
}
export const RadioButton = styled.button.attrs(({ selected }: RadioButtonProps) => ({
@ -29,7 +30,7 @@ export const RadioButton = styled.button.attrs(({ selected }: RadioButtonProps)
${({ selected }) => (
selected
? `
? css`
background-color: #666666;
color: #ffffff;
@ -43,12 +44,13 @@ export const RadioButton = styled.button.attrs(({ selected }: RadioButtonProps)
}
:last-child {
background: transparent;
color: #ffffff;
border: 1px solid #fff;
}
}
`
: `
: css`
background-color: #3F3F3F;
color: #999999;
@ -58,9 +60,9 @@ export const RadioButton = styled.button.attrs(({ selected }: RadioButtonProps)
`
)}
:first-child {
text-transform: uppercase;
}
${({ upperCase }) => (
upperCase ? 'text-transform: uppercase;' : ''
)}
:not(:last-child) {
border-right: 1px solid #222222;

@ -9,3 +9,4 @@ export * from './StarIcon'
export * from './customScrollbar'
export * from './customStyles'
export * from './Image'
export * from './RadioButtons'

@ -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,18 +1,19 @@
import React from 'react'
import { T9n } from 'features/T9n'
import { MatchStatuses, useHeaderFiltersStore } from '../../store'
import {
RadioButtonGroup,
RadioButton,
} from './styled'
} from 'features/Common'
import { MatchStatuses, useHeaderFiltersStore } from '../../store'
export const MatchStatusFilter = () => {
const { selectedMatchStatus, setSelectedMatchStatus } = useHeaderFiltersStore()
return (
<RadioButtonGroup>
<RadioButton
upperCase
selected={selectedMatchStatus === MatchStatuses.Live}
onClick={() => setSelectedMatchStatus(MatchStatuses.Live)}
buttonWidth={80}

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

@ -45,6 +45,7 @@ export const DropdownButton = styled.button<Props>`
padding-right: 45px;
outline: none;
border: none;
border-radius: 2px;
display: flex;
align-items: center;
background-color: #3F3F3F;
@ -114,6 +115,7 @@ export const Wrapper = styled.div`
position: relative;
${DropdownButton} {
border-radius: 0;
border-top-right-radius: 2px;
border-bottom-right-radius: 2px;
}

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

@ -3,41 +3,64 @@ import { useMediaQuery } from 'react-responsive'
import { devices } from 'config'
import { HeaderMobile } from 'features/HeaderMobile'
import { Matches } from 'features/Matches'
import {
HeaderFiltersStore,
DateFilter,
MatchStatusFilter,
TournamentFilter,
SportTypeFilter,
} from 'features/HeaderFilters'
import {
HeaderMobileMidle,
HeaderMobileBottom,
} from 'features/HeaderMobile/styled'
import { SportFilterWrapper } from 'features/ProfileHeader/styled'
import { useHomePage } from './hooks'
import { Header } from './components/Header'
import { Content } from './styled'
export const HomePage = () => {
const Home = () => {
const isMobile = useMediaQuery({ query: devices.tablet })
const { fetchMatches } = useHomePage()
return (
<Fragment>
{
isMobile
? (
<Fragment>
<HeaderMobileMidle>
<DateFilter />
</HeaderMobileMidle>
<HeaderMobileBottom>
<MatchStatusFilter />
</HeaderMobileBottom>
</Fragment>
)
: null
? <HeaderMobile />
: <Header />
}
{
isMobile && (
<Fragment>
<HeaderMobileMidle>
<DateFilter />
</HeaderMobileMidle>
<HeaderMobileBottom>
<MatchStatusFilter />
</HeaderMobileBottom>
</Fragment>
)
}
<Content>
<Matches fetch={fetchMatches} />
</Content>
{
isMobile && (
<SportFilterWrapper>
<SportTypeFilter />
<TournamentFilter />
</SportFilterWrapper>
)
}
</Fragment>
)
}
export const HomePage = () => (
<HeaderFiltersStore>
<Home />
</HeaderFiltersStore>
)

@ -27,7 +27,8 @@ type Name = {
}
type SearchItemsListProps = {
close: () => void,
className?: string,
close?: () => void,
list: Array<{
gender?: Gender,
id: number,
@ -40,6 +41,7 @@ type SearchItemsListProps = {
}
export const ItemsList = ({
className,
close,
list,
profileType,
@ -47,7 +49,7 @@ export const ItemsList = ({
const { ref } = useItemsList()
return (
<Wrapper ref={ref}>
<Wrapper className={className} ref={ref}>
{map(list, (item) => (
<Item key={item.id}>
<StyledLink

@ -1,9 +1,10 @@
import React from 'react'
import React, { Fragment } from 'react'
import isEmpty from 'lodash/isEmpty'
import { StreamPlayer } from 'features/StreamPlayer'
import { MultiSourcePlayer } from 'features/MultiSourcePlayer'
import { ProfileHeader } from 'features/ProfileHeader'
import { MatchProfileCard } from './MatchProfileCard'
import { usePlayerProgressReporter } from './hooks/usePlayerProgressReporter'
@ -25,30 +26,33 @@ export const MatchPage = () => {
const isFinishedMatch = !isEmpty(videos) && !isLastPlayPositionFetching
return (
<MainWrapper>
<MatchProfileCard profile={profile} />
<Container>
{
isLiveMatch && (
<StreamPlayer
url={url}
onPlayingChange={onPlayingChange}
onProgressChange={onPlayerProgressChange}
resumeFrom={lastPlayPosition.second}
/>
)
}
{
isFinishedMatch && (
<MultiSourcePlayer
videos={videos}
onPlayingChange={onPlayingChange}
onProgressChange={onPlayerProgressChange}
resumeFrom={lastPlayPosition}
/>
)
}
</Container>
</MainWrapper>
<Fragment>
<ProfileHeader />
<MainWrapper>
<MatchProfileCard profile={profile} />
<Container>
{
isLiveMatch && (
<StreamPlayer
url={url}
onPlayingChange={onPlayingChange}
onProgressChange={onPlayerProgressChange}
resumeFrom={lastPlayPosition.second}
/>
)
}
{
isFinishedMatch && (
<MultiSourcePlayer
videos={videos}
onPlayingChange={onPlayingChange}
onProgressChange={onPlayerProgressChange}
resumeFrom={lastPlayPosition}
/>
)
}
</Container>
</MainWrapper>
</Fragment>
)
}

@ -1,11 +1,13 @@
import React from 'react'
import React, { Fragment } from 'react'
import { ProfileTypes } from 'config'
import { ProfileHeader } from 'features/ProfileHeader'
import { ProfileCard } from 'features/ProfileCard'
import { Matches } from 'features/Matches'
import { Content } from './styled'
import { usePlayerPage } from './hooks'
import { Content } from './styled'
export const PlayerPage = () => {
const {
@ -15,13 +17,16 @@ export const PlayerPage = () => {
} = usePlayerPage()
return (
<Content>
<ProfileCard
profileType={ProfileTypes.PLAYERS}
titleObj={titleObj}
infoItems={infoItems}
/>
<Matches fetch={fetchMatches} />
</Content>
<Fragment>
<ProfileHeader />
<Content>
<ProfileCard
profileType={ProfileTypes.PLAYERS}
titleObj={titleObj}
infoItems={infoItems}
/>
<Matches fetch={fetchMatches} />
</Content>
</Fragment>
)
}

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

@ -9,7 +9,7 @@ export const HomeButtonLink = styled(Link)`
background-image: url('/images/home-btn.svg');
background-repeat: no-repeat;
background-position: center;
&:hover {
background-image: url('/images/home-btn-hover.svg');
cursor:pointer;
@ -35,11 +35,6 @@ export const FilterWrapper = styled.div`
`
export const SearchWrapper = styled(FilterWrapper)`
width: 288px;
height: 48px;
margin-right: 16px;
display: flex;
@media ${devices.desktop} {
width: 51px;
margin-right: 9px;

@ -51,7 +51,7 @@ export const ProfileLogo = ({
<Image
alt={alt || altName}
src={lazy ? '' : src}
data-src={lazy ? src : ''}
dataSrc={lazy ? src : ''}
fallbackSrc={fallbackSrc}
className={className}
title={titleText}

@ -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) => ({

@ -1,14 +1,12 @@
import type { FormEvent, ChangeEvent } from 'react'
import {
useState,
FocusEvent,
useRef,
useCallback,
} from 'react'
import trim from 'lodash/trim'
import debounce from 'lodash/debounce'
import isEmpty from 'lodash/isEmpty'
import size from 'lodash/size'
import type { SearchItems } from 'requests'
@ -19,7 +17,7 @@ import {
} from 'hooks'
import { SEARCH_DELAY, MIN_CHARACTERS_LENGTH } from '../config'
import { normalizeItems } from './useNormalizedItems'
import { normalizeItems } from '../helpers'
export const useSearch = () => {
const [searchItems, setSearchItems] = useState<SearchItems>({})
@ -72,12 +70,6 @@ export const useSearch = () => {
open,
])
const onFocus = useCallback(({ target: { value } }: FocusEvent<HTMLInputElement>) => {
if (size(value) >= MIN_CHARACTERS_LENGTH) {
open()
}
}, [open])
const onSubmit = useCallback((e: FormEvent<HTMLFormElement>) => {
e.preventDefault()
}, [])
@ -87,8 +79,8 @@ export const useSearch = () => {
isFetching,
normalizedItems: normalizeItems(searchItems),
onChange,
onFocus,
onFocus: open,
onSubmit,
showResults: isOpen && !isEmpty(searchItems),
showResults: isOpen,
}
}

@ -4,6 +4,7 @@ import isEmpty from 'lodash/isEmpty'
import { PAGES, ProfileTypes } from 'config'
import { T9n } from 'features/T9n'
import { Input } from 'features/Common'
import { ItemsList } from 'features/ItemsList'
import { OutsideClick } from 'features/OutsideClick'
@ -16,6 +17,9 @@ import {
Form,
Results,
LoaderWrapper,
ResultsWrapper,
LinkWrapper,
StyledLink,
} from './styled'
export const Search = () => {
@ -51,40 +55,47 @@ export const Search = () => {
)}
</Form>
{showResults && (
<Results>
{!isEmpty(tournaments) && (
<Fragment>
<Header title='tournament' image='tournament' />
<ItemsList
list={tournaments}
close={close}
profileType={ProfileTypes.TOURNAMENTS}
/>
</Fragment>
)}
<ResultsWrapper>
<Results>
{!isEmpty(tournaments) && (
<Fragment>
<Header title='tournament' image='tournament' />
<ItemsList
list={tournaments}
close={close}
profileType={ProfileTypes.TOURNAMENTS}
/>
</Fragment>
)}
{!isEmpty(teams) && (
<Fragment>
<Header title='team' image='team' />
<ItemsList
list={teams}
close={close}
profileType={ProfileTypes.TEAMS}
/>
</Fragment>
)}
{!isEmpty(teams) && (
<Fragment>
<Header title='team' image='team' />
<ItemsList
list={teams}
close={close}
profileType={ProfileTypes.TEAMS}
/>
</Fragment>
)}
{!isEmpty(players) && (
<Fragment>
<Header title='player' image='player' />
<ItemsList
list={players}
close={close}
profileType={ProfileTypes.PLAYERS}
/>
</Fragment>
)}
</Results>
{!isEmpty(players) && (
<Fragment>
<Header title='player' image='player' />
<ItemsList
list={players}
close={close}
profileType={ProfileTypes.PLAYERS}
/>
</Fragment>
)}
</Results>
<LinkWrapper>
<StyledLink to={PAGES.extendedSearch}>
<T9n t='go_to_extended_search_page' />
</StyledLink>
</LinkWrapper>
</ResultsWrapper>
)}
</Wrapper>
</OutsideClick>

@ -1,7 +1,9 @@
import { Link } from 'react-router-dom'
import styled from 'styled-components/macro'
import { devices } from 'config/devices'
import { customScrollbar } from 'features/Common'
import { customScrollbar, solidButtonStyles } from 'features/Common'
import {
InputWrapper,
InputStyled,
@ -33,7 +35,6 @@ export const LoaderWrapper = styled.div`
top: 26px;
height: 28px;
}
`
export const Form = styled.form<{isMatch: boolean}>`
@ -53,12 +54,12 @@ export const Form = styled.form<{isMatch: boolean}>`
margin-top: 25px;
background-color: transparent;
}
@media ${devices.mobile} {
margin-top: 20px;
}
}
@media ${devices.desktop} {
width: ${({ isMatch }) => (isMatch ? '244px' : '51px')};
@ -83,7 +84,7 @@ export const Form = styled.form<{isMatch: boolean}>`
top: -33px;
}
}
${InputStyled} {
width: 100%;
@ -91,7 +92,7 @@ export const Form = styled.form<{isMatch: boolean}>`
::-webkit-search-cancel-button,
::-webkit-search-results-button,
::-webkit-search-results-decoration {
display: none;
display: none;
}
@media ${devices.tablet} {
@ -122,7 +123,7 @@ export const Form = styled.form<{isMatch: boolean}>`
}
}
}
:focus-within {
${InputWrapper} {
padding-left: 0;
@ -131,7 +132,7 @@ export const Form = styled.form<{isMatch: boolean}>`
background-color: #3F3F3F;
}
}
${Label} {
::before {
display: none;
@ -145,15 +146,19 @@ export const Form = styled.form<{isMatch: boolean}>`
max-width: 335px;
}
}
`
export const Results = styled.div`
overflow-y: auto;
max-height: 431px;
${customScrollbar}
`
export const ResultsWrapper = styled.div`
position: absolute;
top: 56px;
width: 448px;
max-height: 431px;
overflow-y: auto;
z-index: 1;
@media ${devices.tablet} {
@ -166,6 +171,24 @@ export const Results = styled.div`
left: 0;
width: 100vw;
}
`
${customScrollbar}
export const LinkWrapper = styled.div`
background-color: #666;
width: 440px;
padding: 10px 0;
`
export const StyledLink = styled(Link)`
${solidButtonStyles}
width: 217px;
height: 30px;
font-weight: 600;
font-size: 11px;
margin: 0 auto;
padding: 0;
display: flex;
justify-content: center;
align-items: center;
`

@ -3,18 +3,28 @@ import { useState, useEffect } from 'react'
import find from 'lodash/find'
import { SportTypes } from 'config'
import type { SportList } from 'requests/getSportList'
import { getSportList } from 'requests/getSportList'
import { useHeaderFiltersStore } from 'features/HeaderFilters/store'
import { useToggle } from 'hooks'
export const useSportTypeFilter = () => {
import type { CustomStyles } from 'features/Common'
export type Props = {
onReset: () => void,
onSelect: (sport: SportTypes) => void,
selectedSportId: SportTypes | null,
wrapperStyles?: CustomStyles,
}
export const useSportTypeFilter = ({
onReset,
onSelect,
selectedSportId,
}: Props) => {
const [sportList, setSportList] = useState<SportList>([])
const {
selectedSportTypeId,
setSelectedSportTypeId,
setSelectedTournamentId,
} = useHeaderFiltersStore()
const {
close,
@ -26,24 +36,22 @@ export const useSportTypeFilter = () => {
getSportList().then(setSportList)
}, [])
const onSelect = (id: number) => {
setSelectedSportTypeId(id)
setSelectedTournamentId(null)
const handleSelect = (id: SportTypes) => {
onSelect(id)
close()
}
const onResetSelectedSport = (e: MouseEvent<HTMLButtonElement>) => {
e.stopPropagation()
setSelectedSportTypeId(null)
setSelectedTournamentId(null)
onReset()
}
const selectedSportType = find(sportList, (sport) => sport.id === selectedSportTypeId)
const selectedSportType = find(sportList, (sport) => sport.id === selectedSportId)
return {
close,
isOpen,
onResetSelectedSport,
onSelect,
onSelect: handleSelect,
open,
selectedSportType,
sportList,

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

@ -4,21 +4,13 @@ import toLower from 'lodash/toLower'
import { devices, SportTypes } from 'config'
import { DropdownButton } from '../TournamentFilter/styled'
import { customStylesMixin } from 'features/Common'
export const Wrapper = styled.div`
width: 50%;
position: relative;
border-right: 1px solid #222222;
@media ${devices.tablet} {
border-right: 0;
}
width: 100%;
${DropdownButton} {
border-top-left-radius: 2px;
border-bottom-left-radius: 2px;
}
${customStylesMixin}
`
export const SportList = styled.ul`
@ -39,11 +31,11 @@ export const SportList = styled.ul`
}
`
type CustomOptionProps = {
type SportItemProps = {
sport: SportTypes,
}
export const CustomOption = styled.li<CustomOptionProps>`
export const SportItem = styled.li<SportItemProps>`
width: 100%;
height: 48px;
display: flex;

@ -2,6 +2,7 @@ import React, { Fragment } from 'react'
import { ProfileTypes } from 'config'
import { ProfileHeader } from 'features/ProfileHeader'
import { ProfileCard } from 'features/ProfileCard'
import { Matches } from 'features/Matches'
@ -17,6 +18,7 @@ export const TeamPage = () => {
return (
<Fragment>
<ProfileHeader />
<Content>
<ProfileCard
profileType={ProfileTypes.TEAMS}

@ -2,6 +2,7 @@ import React, { Fragment } from 'react'
import { ProfileTypes } from 'config'
import { ProfileHeader } from 'features/ProfileHeader'
import { ProfileCard } from 'features/ProfileCard'
import { Matches } from 'features/Matches'
@ -17,6 +18,7 @@ export const TournamentPage = () => {
return (
<Fragment>
<ProfileHeader />
<Content>
<ProfileCard
profileType={ProfileTypes.TOURNAMENTS}

@ -5,6 +5,7 @@ export * from './getCountries'
export * from './getCountryCities'
export * from './getLexics'
export * from './getSearchItems'
export * from './search'
export * from './getMatches'
export * from './getUserSportFavs'
export * from './modifyUserSportFavs'

@ -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…
Cancel
Save