From 9b37149cb423f3408dfb849b49fbb0b4d13483dc Mon Sep 17 00:00:00 2001 From: Mirlan Date: Tue, 10 Nov 2020 11:44:48 +0600 Subject: [PATCH] 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 --- src/config/lexics/indexLexics.tsx | 4 + src/config/pages.tsx | 1 + src/features/App/AuthenticatedApp.tsx | 84 +++++-------- .../RadioButtons/index.tsx} | 16 +-- src/features/Common/index.tsx | 1 + .../components/DesktopHeader/index.tsx | 23 ++++ .../components/Filters/index.tsx | 29 +++++ .../components/Filters/styled.tsx | 43 +++++++ .../components/GenderFilter/index.tsx | 36 ++++++ .../components/MobileHeader/index.tsx | 31 +++++ .../components/ProfileFilter/index.tsx | 44 +++++++ .../components/Results/index.tsx | 47 ++++++++ .../components/SearchInput/index.tsx | 54 +++++++++ .../components/SportTypeFilter/index.tsx | 19 +++ src/features/ExtendedSearchPage/index.tsx | 35 ++++++ .../ExtendedSearchPage/store/hooks/index.tsx | 87 ++++++++++++++ .../store/hooks/useSearchRequest.tsx | 53 ++++++++ .../ExtendedSearchPage/store/index.tsx | 19 +++ src/features/ExtendedSearchPage/styled.tsx | 41 +++++++ src/features/Header/index.tsx | 62 ---------- .../components/MatchStatusFilter/index.tsx | 7 +- .../components/SportTypeFilter/index.tsx | 113 ++++++++---------- .../components/TournamentFilter/styled.tsx | 2 + .../HomePage/components/Header/index.tsx | 44 +++++++ src/features/HomePage/index.tsx | 47 ++++++-- src/features/ItemsList/index.tsx | 6 +- src/features/MatchPage/index.tsx | 56 +++++---- src/features/PlayerPage/index.tsx | 25 ++-- src/features/ProfileHeader/index.tsx | 35 ++++++ .../{Header => ProfileHeader}/styled.tsx | 7 +- src/features/ProfileLogo/index.tsx | 2 +- .../useNormalizedItems.tsx => helpers.tsx} | 4 +- src/features/Search/hooks/index.tsx | 14 +-- src/features/Search/index.tsx | 75 +++++++----- src/features/Search/styled.tsx | 47 ++++++-- .../components => }/SportTypeFilter/hooks.tsx | 36 +++--- src/features/SportTypeFilter/index.tsx | 74 ++++++++++++ .../SportTypeFilter/styled.tsx | 18 +-- src/features/TeamPage/index.tsx | 2 + src/features/TournamentPage/index.tsx | 2 + src/requests/index.tsx | 1 + src/requests/search.tsx | 81 +++++++++++++ 42 files changed, 1099 insertions(+), 328 deletions(-) rename src/features/{HeaderFilters/components/MatchStatusFilter/styled.tsx => Common/RadioButtons/index.tsx} (87%) create mode 100644 src/features/ExtendedSearchPage/components/DesktopHeader/index.tsx create mode 100644 src/features/ExtendedSearchPage/components/Filters/index.tsx create mode 100644 src/features/ExtendedSearchPage/components/Filters/styled.tsx create mode 100644 src/features/ExtendedSearchPage/components/GenderFilter/index.tsx create mode 100644 src/features/ExtendedSearchPage/components/MobileHeader/index.tsx create mode 100644 src/features/ExtendedSearchPage/components/ProfileFilter/index.tsx create mode 100644 src/features/ExtendedSearchPage/components/Results/index.tsx create mode 100644 src/features/ExtendedSearchPage/components/SearchInput/index.tsx create mode 100644 src/features/ExtendedSearchPage/components/SportTypeFilter/index.tsx create mode 100644 src/features/ExtendedSearchPage/index.tsx create mode 100644 src/features/ExtendedSearchPage/store/hooks/index.tsx create mode 100644 src/features/ExtendedSearchPage/store/hooks/useSearchRequest.tsx create mode 100644 src/features/ExtendedSearchPage/store/index.tsx create mode 100644 src/features/ExtendedSearchPage/styled.tsx delete mode 100644 src/features/Header/index.tsx create mode 100644 src/features/HomePage/components/Header/index.tsx create mode 100644 src/features/ProfileHeader/index.tsx rename src/features/{Header => ProfileHeader}/styled.tsx (95%) rename src/features/Search/{hooks/useNormalizedItems.tsx => helpers.tsx} (84%) rename src/features/{HeaderFilters/components => }/SportTypeFilter/hooks.tsx (61%) create mode 100644 src/features/SportTypeFilter/index.tsx rename src/features/{HeaderFilters/components => }/SportTypeFilter/styled.tsx (73%) create mode 100644 src/requests/search.tsx diff --git a/src/config/lexics/indexLexics.tsx b/src/config/lexics/indexLexics.tsx index 9990cb70..a4f5ba24 100644 --- a/src/config/lexics/indexLexics.tsx +++ b/src/config/lexics/indexLexics.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, diff --git a/src/config/pages.tsx b/src/config/pages.tsx index affdcc95..aaa94cd9 100644 --- a/src/config/pages.tsx +++ b/src/config/pages.tsx @@ -1,4 +1,5 @@ export const PAGES = { + extendedSearch: '/search', home: '/', login: '/login', match: '/matches', diff --git a/src/features/App/AuthenticatedApp.tsx b/src/features/App/AuthenticatedApp.tsx index 0328370f..8ee87ba8 100644 --- a/src/features/App/AuthenticatedApp.tsx +++ b/src/features/App/AuthenticatedApp.tsx @@ -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 ( { isMobile || isUserAccountPage ? null - : ( - - - - - ) + : } + {isExtendedSearchPage ? null : } - - - - { - isMobile - ? - : ( - - -
- - ) - } - - - + + + {!isMobile && } + + + - - - + + + - - - - - - - - - - { - isMobile - ? ( - - - - - ) - : null - } - - - + + + + + + + + + + + + + + diff --git a/src/features/HeaderFilters/components/MatchStatusFilter/styled.tsx b/src/features/Common/RadioButtons/index.tsx similarity index 87% rename from src/features/HeaderFilters/components/MatchStatusFilter/styled.tsx rename to src/features/Common/RadioButtons/index.tsx index e90421cc..628fa33a 100644 --- a/src/features/HeaderFilters/components/MatchStatusFilter/styled.tsx +++ b/src/features/Common/RadioButtons/index.tsx @@ -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; diff --git a/src/features/Common/index.tsx b/src/features/Common/index.tsx index 6e0bea4f..8e64ad19 100644 --- a/src/features/Common/index.tsx +++ b/src/features/Common/index.tsx @@ -9,3 +9,4 @@ export * from './StarIcon' export * from './customScrollbar' export * from './customStyles' export * from './Image' +export * from './RadioButtons' diff --git a/src/features/ExtendedSearchPage/components/DesktopHeader/index.tsx b/src/features/ExtendedSearchPage/components/DesktopHeader/index.tsx new file mode 100644 index 00000000..49ac8fbb --- /dev/null +++ b/src/features/ExtendedSearchPage/components/DesktopHeader/index.tsx @@ -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 = () => ( + + + + + + + + +) diff --git a/src/features/ExtendedSearchPage/components/Filters/index.tsx b/src/features/ExtendedSearchPage/components/Filters/index.tsx new file mode 100644 index 00000000..747093d7 --- /dev/null +++ b/src/features/ExtendedSearchPage/components/Filters/index.tsx @@ -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 = () => ( + + + + + + + + + + + + + + + +) diff --git a/src/features/ExtendedSearchPage/components/Filters/styled.tsx b/src/features/ExtendedSearchPage/components/Filters/styled.tsx new file mode 100644 index 00000000..932d0b8d --- /dev/null +++ b/src/features/ExtendedSearchPage/components/Filters/styled.tsx @@ -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; +` diff --git a/src/features/ExtendedSearchPage/components/GenderFilter/index.tsx b/src/features/ExtendedSearchPage/components/GenderFilter/index.tsx new file mode 100644 index 00000000..deadc86e --- /dev/null +++ b/src/features/ExtendedSearchPage/components/GenderFilter/index.tsx @@ -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 ( + + onGenderChange(Gender.MALE)} + buttonWidth={122} + > + + + onGenderChange(Gender.FEMALE)} + buttonWidth={122} + > + + + + ) +} diff --git a/src/features/ExtendedSearchPage/components/MobileHeader/index.tsx b/src/features/ExtendedSearchPage/components/MobileHeader/index.tsx new file mode 100644 index 00000000..35c723b7 --- /dev/null +++ b/src/features/ExtendedSearchPage/components/MobileHeader/index.tsx @@ -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 = () => ( + + + + + + + + + + + + + +) diff --git a/src/features/ExtendedSearchPage/components/ProfileFilter/index.tsx b/src/features/ExtendedSearchPage/components/ProfileFilter/index.tsx new file mode 100644 index 00000000..806202d1 --- /dev/null +++ b/src/features/ExtendedSearchPage/components/ProfileFilter/index.tsx @@ -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 ( + + onProfileChange(ProfileTypes.TEAMS)} + buttonWidth={122} + > + + + onProfileChange(ProfileTypes.PLAYERS)} + buttonWidth={122} + > + + + onProfileChange(ProfileTypes.TOURNAMENTS)} + buttonWidth={122} + > + + + + ) +} diff --git a/src/features/ExtendedSearchPage/components/Results/index.tsx b/src/features/ExtendedSearchPage/components/Results/index.tsx new file mode 100644 index 00000000..8fe2169b --- /dev/null +++ b/src/features/ExtendedSearchPage/components/Results/index.tsx @@ -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 ( + + + <ResultsList + list={results.teams} + profileType={ProfileTypes.TEAMS} + /> + <ResultsList + list={results.players} + profileType={ProfileTypes.PLAYERS} + /> + <ResultsList + list={results.tournaments} + profileType={ProfileTypes.TOURNAMENTS} + /> + </Fragment> + ) + } + return null +}) diff --git a/src/features/ExtendedSearchPage/components/SearchInput/index.tsx b/src/features/ExtendedSearchPage/components/SearchInput/index.tsx new file mode 100644 index 00000000..75e7de0e --- /dev/null +++ b/src/features/ExtendedSearchPage/components/SearchInput/index.tsx @@ -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> + ) +} diff --git a/src/features/ExtendedSearchPage/components/SportTypeFilter/index.tsx b/src/features/ExtendedSearchPage/components/SportTypeFilter/index.tsx new file mode 100644 index 00000000..967488a0 --- /dev/null +++ b/src/features/ExtendedSearchPage/components/SportTypeFilter/index.tsx @@ -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)} + /> + ) +} diff --git a/src/features/ExtendedSearchPage/index.tsx b/src/features/ExtendedSearchPage/index.tsx new file mode 100644 index 00000000..2a45ffed --- /dev/null +++ b/src/features/ExtendedSearchPage/index.tsx @@ -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> +) diff --git a/src/features/ExtendedSearchPage/store/hooks/index.tsx b/src/features/ExtendedSearchPage/store/hooks/index.tsx new file mode 100644 index 00000000..bb7fc004 --- /dev/null +++ b/src/features/ExtendedSearchPage/store/hooks/index.tsx @@ -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, + } +} diff --git a/src/features/ExtendedSearchPage/store/hooks/useSearchRequest.tsx b/src/features/ExtendedSearchPage/store/hooks/useSearchRequest.tsx new file mode 100644 index 00000000..64c8ba59 --- /dev/null +++ b/src/features/ExtendedSearchPage/store/hooks/useSearchRequest.tsx @@ -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, + } +} diff --git a/src/features/ExtendedSearchPage/store/index.tsx b/src/features/ExtendedSearchPage/store/index.tsx new file mode 100644 index 00000000..c30e4c5f --- /dev/null +++ b/src/features/ExtendedSearchPage/store/index.tsx @@ -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) diff --git a/src/features/ExtendedSearchPage/styled.tsx b/src/features/ExtendedSearchPage/styled.tsx new file mode 100644 index 00000000..18ac8e46 --- /dev/null +++ b/src/features/ExtendedSearchPage/styled.tsx @@ -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; + } +` diff --git a/src/features/Header/index.tsx b/src/features/Header/index.tsx deleted file mode 100644 index 27bc4943..00000000 --- a/src/features/Header/index.tsx +++ /dev/null @@ -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> -) diff --git a/src/features/HeaderFilters/components/MatchStatusFilter/index.tsx b/src/features/HeaderFilters/components/MatchStatusFilter/index.tsx index 963c9b5a..5ea763a1 100644 --- a/src/features/HeaderFilters/components/MatchStatusFilter/index.tsx +++ b/src/features/HeaderFilters/components/MatchStatusFilter/index.tsx @@ -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} diff --git a/src/features/HeaderFilters/components/SportTypeFilter/index.tsx b/src/features/HeaderFilters/components/SportTypeFilter/index.tsx index 506f5864..55498e5d 100644 --- a/src/features/HeaderFilters/components/SportTypeFilter/index.tsx +++ b/src/features/HeaderFilters/components/SportTypeFilter/index.tsx @@ -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} + /> ) } diff --git a/src/features/HeaderFilters/components/TournamentFilter/styled.tsx b/src/features/HeaderFilters/components/TournamentFilter/styled.tsx index ab6bd535..025ec45a 100644 --- a/src/features/HeaderFilters/components/TournamentFilter/styled.tsx +++ b/src/features/HeaderFilters/components/TournamentFilter/styled.tsx @@ -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; } diff --git a/src/features/HomePage/components/Header/index.tsx b/src/features/HomePage/components/Header/index.tsx new file mode 100644 index 00000000..4ae4c626 --- /dev/null +++ b/src/features/HomePage/components/Header/index.tsx @@ -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> +) diff --git a/src/features/HomePage/index.tsx b/src/features/HomePage/index.tsx index 8b969735..cb227fe6 100644 --- a/src/features/HomePage/index.tsx +++ b/src/features/HomePage/index.tsx @@ -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> +) diff --git a/src/features/ItemsList/index.tsx b/src/features/ItemsList/index.tsx index b8f25460..812ca9a1 100644 --- a/src/features/ItemsList/index.tsx +++ b/src/features/ItemsList/index.tsx @@ -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 diff --git a/src/features/MatchPage/index.tsx b/src/features/MatchPage/index.tsx index 523550e5..575efd62 100644 --- a/src/features/MatchPage/index.tsx +++ b/src/features/MatchPage/index.tsx @@ -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> ) } diff --git a/src/features/PlayerPage/index.tsx b/src/features/PlayerPage/index.tsx index b4ed576a..b9c7ff57 100644 --- a/src/features/PlayerPage/index.tsx +++ b/src/features/PlayerPage/index.tsx @@ -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> ) } diff --git a/src/features/ProfileHeader/index.tsx b/src/features/ProfileHeader/index.tsx new file mode 100644 index 00000000..b22bb0f7 --- /dev/null +++ b/src/features/ProfileHeader/index.tsx @@ -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> + ) + ) +} diff --git a/src/features/Header/styled.tsx b/src/features/ProfileHeader/styled.tsx similarity index 95% rename from src/features/Header/styled.tsx rename to src/features/ProfileHeader/styled.tsx index e9ee6b6e..003de4fa 100644 --- a/src/features/Header/styled.tsx +++ b/src/features/ProfileHeader/styled.tsx @@ -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; diff --git a/src/features/ProfileLogo/index.tsx b/src/features/ProfileLogo/index.tsx index 170d3b89..0d4b8f35 100644 --- a/src/features/ProfileLogo/index.tsx +++ b/src/features/ProfileLogo/index.tsx @@ -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} diff --git a/src/features/Search/hooks/useNormalizedItems.tsx b/src/features/Search/helpers.tsx similarity index 84% rename from src/features/Search/hooks/useNormalizedItems.tsx rename to src/features/Search/helpers.tsx index 193dd387..82081f63 100644 --- a/src/features/Search/hooks/useNormalizedItems.tsx +++ b/src/features/Search/helpers.tsx @@ -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) => ({ diff --git a/src/features/Search/hooks/index.tsx b/src/features/Search/hooks/index.tsx index b981e8cd..bc99052b 100644 --- a/src/features/Search/hooks/index.tsx +++ b/src/features/Search/hooks/index.tsx @@ -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, } } diff --git a/src/features/Search/index.tsx b/src/features/Search/index.tsx index 195dc10e..3fca7528 100644 --- a/src/features/Search/index.tsx +++ b/src/features/Search/index.tsx @@ -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> diff --git a/src/features/Search/styled.tsx b/src/features/Search/styled.tsx index 44ac376d..7e8d02c9 100644 --- a/src/features/Search/styled.tsx +++ b/src/features/Search/styled.tsx @@ -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; ` diff --git a/src/features/HeaderFilters/components/SportTypeFilter/hooks.tsx b/src/features/SportTypeFilter/hooks.tsx similarity index 61% rename from src/features/HeaderFilters/components/SportTypeFilter/hooks.tsx rename to src/features/SportTypeFilter/hooks.tsx index cc0be53d..38d5dfb5 100644 --- a/src/features/HeaderFilters/components/SportTypeFilter/hooks.tsx +++ b/src/features/SportTypeFilter/hooks.tsx @@ -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, diff --git a/src/features/SportTypeFilter/index.tsx b/src/features/SportTypeFilter/index.tsx new file mode 100644 index 00000000..09b70b76 --- /dev/null +++ b/src/features/SportTypeFilter/index.tsx @@ -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> + ) +} diff --git a/src/features/HeaderFilters/components/SportTypeFilter/styled.tsx b/src/features/SportTypeFilter/styled.tsx similarity index 73% rename from src/features/HeaderFilters/components/SportTypeFilter/styled.tsx rename to src/features/SportTypeFilter/styled.tsx index eb398abb..e0610746 100644 --- a/src/features/HeaderFilters/components/SportTypeFilter/styled.tsx +++ b/src/features/SportTypeFilter/styled.tsx @@ -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; diff --git a/src/features/TeamPage/index.tsx b/src/features/TeamPage/index.tsx index 5951c300..3d54ff51 100644 --- a/src/features/TeamPage/index.tsx +++ b/src/features/TeamPage/index.tsx @@ -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} diff --git a/src/features/TournamentPage/index.tsx b/src/features/TournamentPage/index.tsx index 4458f47c..94cbd463 100644 --- a/src/features/TournamentPage/index.tsx +++ b/src/features/TournamentPage/index.tsx @@ -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} diff --git a/src/requests/index.tsx b/src/requests/index.tsx index 170f59d1..1bdac86c 100644 --- a/src/requests/index.tsx +++ b/src/requests/index.tsx @@ -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' diff --git a/src/requests/search.tsx b/src/requests/search.tsx new file mode 100644 index 00000000..df2f3e1e --- /dev/null +++ b/src/requests/search.tsx @@ -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, + }) +}