Preprod (#495)
* feat(ott-1637): change in match popup and match events requests (#488) * Ott 1118 user match preferences (#492) * Ott 1118 part 1 (#469) * fix(1118): wip * feat(1118): tournaments block * feat(1118): resolved comments * feat(1118): show and search tournaments (#472) feat(1118): show loader on fetch * Ott 1118 part 3 (#478) * feat(1118): added lexics * feat(1118): wip, load and save prefs * feat(1118): virtualization using list * Ott 1118 part 4 (#487) * fix(1118): reverted useRequest hook change * fix(1118): px -> rem * fix(1118): request matches after apply * fix(1118): wip * refactor(1118): removed old filters from HeaderFiltersStore (#491) * fix(1118): disabled preferences (#493) * fix(ott-1662): add new langs (#494) Co-authored-by: PolyakovaM <55061222+PolyakovaM@users.noreply.github.com> Co-authored-by: Serg <936x936@gmail.com>keep-around/af30b88d367751c9e05a735e4a0467a96238ef47
parent
e7f20c0ccb
commit
92599ec976
@ -1,6 +1,7 @@ |
||||
export type ArrowLoaderProps = { |
||||
backgroundColor?: string, |
||||
backgroundSize?: string, |
||||
className?: string, |
||||
disabled?: boolean, |
||||
width?:string, |
||||
} |
||||
|
||||
@ -1,15 +0,0 @@ |
||||
import { MatchStatuses } from 'features/HeaderFilters/store/hooks' |
||||
|
||||
import { isValidMatchStatus } from '..' |
||||
|
||||
it('returns true for valid match statuses', () => { |
||||
expect(isValidMatchStatus(MatchStatuses.Finished)).toBe(true) |
||||
expect(isValidMatchStatus(MatchStatuses.Live)).toBe(true) |
||||
expect(isValidMatchStatus(MatchStatuses.Soon)).toBe(true) |
||||
}) |
||||
|
||||
it('returns false for invalid match statuses', () => { |
||||
expect(isValidMatchStatus(-1)).toBe(false) |
||||
expect(isValidMatchStatus(0)).toBe(false) |
||||
expect(isValidMatchStatus(4)).toBe(false) |
||||
}) |
||||
@ -1,9 +0,0 @@ |
||||
import isNumber from 'lodash/isNumber' |
||||
import includes from 'lodash/includes' |
||||
import values from 'lodash/values' |
||||
|
||||
import { MatchStatuses } from '../../hooks' |
||||
|
||||
export const isValidMatchStatus = (value: number | null) => ( |
||||
isNumber(value) && includes(values(MatchStatuses), value) |
||||
) |
||||
@ -1,15 +0,0 @@ |
||||
import { SportTypes } from 'config' |
||||
|
||||
import { isValidSportType } from '..' |
||||
|
||||
it('returns true for valid sport types', () => { |
||||
expect(isValidSportType(SportTypes.BASKETBALL)).toBe(true) |
||||
expect(isValidSportType(SportTypes.FOOTBALL)).toBe(true) |
||||
expect(isValidSportType(SportTypes.HOCKEY)).toBe(true) |
||||
}) |
||||
|
||||
it('returns false for invalid sport types', () => { |
||||
expect(isValidSportType(-1)).toBe(false) |
||||
expect(isValidSportType(0)).toBe(false) |
||||
expect(isValidSportType(4)).toBe(false) |
||||
}) |
||||
@ -1,9 +0,0 @@ |
||||
import isNumber from 'lodash/isNumber' |
||||
import includes from 'lodash/includes' |
||||
import values from 'lodash/values' |
||||
|
||||
import { SportTypes } from 'config' |
||||
|
||||
export const isValidSportType = (value: number | null) => ( |
||||
isNumber(value) && includes(values(SportTypes), value) |
||||
) |
||||
@ -1,82 +1,19 @@ |
||||
import { useCallback } from 'react' |
||||
|
||||
import filter from 'lodash/filter' |
||||
import isPast from 'date-fns/isPast' |
||||
import differenceInMinutes from 'date-fns/differenceInMinutes' |
||||
import { getHomeMatches } from 'requests' |
||||
|
||||
import { |
||||
getHomeMatches, |
||||
Matches, |
||||
MatchesBySection, |
||||
} from 'requests' |
||||
|
||||
import { MatchStatuses, useHeaderFiltersStore } from 'features/HeaderFilters' |
||||
import { useMatchSwitchesStore } from 'features/MatchSwitches' |
||||
|
||||
const matchesFilteredByStatus = (matches : Matches, status : MatchStatuses | null) => { |
||||
if (!status) return matches |
||||
const filteredMatches = filter(matches, (match) => { |
||||
const matchDate = new Date(match.date) |
||||
const matchIsStarted = isPast(matchDate) |
||||
const difTime = differenceInMinutes(new Date(), matchDate) |
||||
|
||||
switch (status) { |
||||
case MatchStatuses.Soon: |
||||
return !matchIsStarted && (difTime > -60) |
||||
case MatchStatuses.Live: |
||||
return match.live && matchIsStarted |
||||
case MatchStatuses.Finished: |
||||
return matchIsStarted && (match.storage || match.has_video) |
||||
default: return false |
||||
} |
||||
}) |
||||
return filteredMatches |
||||
} |
||||
|
||||
const setMatches = ( |
||||
matches : MatchesBySection, |
||||
status : MatchStatuses | null, |
||||
): MatchesBySection => { |
||||
if (matches.isVideoSections) { |
||||
return { |
||||
...matches, |
||||
broadcast: matchesFilteredByStatus(matches.broadcast, status), |
||||
features: matchesFilteredByStatus(matches.features, status), |
||||
highlights: matchesFilteredByStatus(matches.highlights, status), |
||||
} |
||||
} return { |
||||
...matches, |
||||
broadcast: matchesFilteredByStatus(matches.broadcast, status), |
||||
} |
||||
} |
||||
import { useHeaderFiltersStore } from 'features/HeaderFilters' |
||||
|
||||
export const useHomePage = () => { |
||||
const { |
||||
selectedDateFormatted, |
||||
selectedMatchStatus, |
||||
selectedSportTypeId, |
||||
selectedTournamentId, |
||||
} = useHeaderFiltersStore() |
||||
const { availableMatchesOnly } = useMatchSwitchesStore() |
||||
const { selectedDateFormatted } = useHeaderFiltersStore() |
||||
|
||||
const fetchMatches = useCallback( |
||||
(limit: number, offset: number) => getHomeMatches({ |
||||
availableMatchesOnly, |
||||
date: selectedDateFormatted, |
||||
limit, |
||||
matchStatus: null, |
||||
offset, |
||||
sportType: selectedSportTypeId, |
||||
tournamentId: selectedTournamentId, |
||||
}) |
||||
.then((matches) => setMatches(matches, selectedMatchStatus)), |
||||
[ |
||||
selectedDateFormatted, |
||||
selectedMatchStatus, |
||||
selectedSportTypeId, |
||||
selectedTournamentId, |
||||
availableMatchesOnly, |
||||
], |
||||
}), |
||||
[selectedDateFormatted], |
||||
) |
||||
return { fetchMatches } |
||||
} |
||||
|
||||
@ -0,0 +1,74 @@ |
||||
import styled from 'styled-components/macro' |
||||
|
||||
import { ClearButton } from 'features/Search/styled' |
||||
import { usePreferencesStore } from 'features/PreferencesPopup/store' |
||||
import { useLexicsStore } from 'features/LexicsStore' |
||||
|
||||
const Wrapper = styled.div` |
||||
position: relative; |
||||
width: 100%; |
||||
height: 36px; |
||||
margin-right: 5px; |
||||
background: #292929; |
||||
border-radius: 10px; |
||||
display: flex; |
||||
align-items: center; |
||||
|
||||
::before { |
||||
content: ''; |
||||
display: block; |
||||
position: absolute; |
||||
top: 50%; |
||||
margin-left: 7px; |
||||
transform: translateY(-50%); |
||||
width: 20px; |
||||
height: 20px; |
||||
background-image: url(/images/search.svg); |
||||
background-size: 14px; |
||||
background-repeat: no-repeat; |
||||
background-position: center; |
||||
cursor: pointer; |
||||
} |
||||
` |
||||
|
||||
const Input = styled.input` |
||||
border: none; |
||||
background: transparent; |
||||
outline: none; |
||||
position: relative; |
||||
|
||||
width: 100%; |
||||
height: 100%; |
||||
display: block; |
||||
margin-bottom: 2px; |
||||
padding-left: 34px; |
||||
color: ${({ theme }) => theme.colors.text100}; |
||||
caret-color: rgba(255, 255, 255, 0.5); |
||||
font-weight: normal; |
||||
font-size: 0.66rem; |
||||
line-height: 1.04rem; |
||||
letter-spacing: -0.4px; |
||||
|
||||
::placeholder { |
||||
color: rgba(255, 255, 255, 0.5); |
||||
} |
||||
` |
||||
|
||||
export const Search = () => { |
||||
const { translate } = useLexicsStore() |
||||
const { |
||||
clearQuery, |
||||
onQueryChange, |
||||
query, |
||||
} = usePreferencesStore() |
||||
return ( |
||||
<Wrapper> |
||||
<Input |
||||
placeholder={translate('search')} |
||||
value={query} |
||||
onChange={onQueryChange} |
||||
/> |
||||
{query && <ClearButton onClick={clearQuery} />} |
||||
</Wrapper> |
||||
) |
||||
} |
||||
@ -0,0 +1,39 @@ |
||||
import map from 'lodash/map' |
||||
import includes from 'lodash/includes' |
||||
|
||||
import { usePreferencesStore } from 'features/PreferencesPopup/store' |
||||
import { T9n } from 'features/T9n' |
||||
|
||||
import { BlockTitle } from '../../styled' |
||||
import { |
||||
Wrapper, |
||||
List, |
||||
Item, |
||||
Checkbox, |
||||
} from './styled' |
||||
|
||||
export const SportsList = () => { |
||||
const { |
||||
onSportSelect, |
||||
selectedSports, |
||||
sports, |
||||
} = usePreferencesStore() |
||||
return ( |
||||
<Wrapper> |
||||
<BlockTitle> |
||||
<T9n t='sport_types' /> |
||||
</BlockTitle> |
||||
<List> |
||||
{map(sports, (sport) => ( |
||||
<Item key={sport.id}> |
||||
<Checkbox |
||||
checked={includes(selectedSports, sport.id)} |
||||
labelLexic={sport.lexic} |
||||
onChange={() => onSportSelect(sport.id)} |
||||
/> |
||||
</Item> |
||||
))} |
||||
</List> |
||||
</Wrapper> |
||||
) |
||||
} |
||||
@ -0,0 +1,39 @@ |
||||
import styled from 'styled-components/macro' |
||||
|
||||
import { devices } from 'config' |
||||
|
||||
import { Checkbox as BaseCheckbox } from 'features/Common/Checkbox' |
||||
import { Label } from 'features/Common/Checkbox/styled' |
||||
import { CheckboxSvg } from 'features/Common/Checkbox/Icon' |
||||
|
||||
export const Wrapper = styled.div` |
||||
min-width: 264px; |
||||
display: flex; |
||||
flex-direction: column; |
||||
|
||||
@media ${devices.tablet} { |
||||
min-width: 13rem; |
||||
} |
||||
` |
||||
|
||||
export const List = styled.ul` |
||||
width: 100%; |
||||
margin-top: 6px; |
||||
` |
||||
|
||||
export const Item = styled.li` |
||||
width: 100%; |
||||
display: flex; |
||||
padding: 6px 0; |
||||
` |
||||
|
||||
export const Checkbox = styled(BaseCheckbox)` |
||||
${Label} { |
||||
font-weight: normal; |
||||
font-size: 0.66rem; |
||||
line-height: 1rem; |
||||
} |
||||
${CheckboxSvg} { |
||||
margin-right: 12px; |
||||
} |
||||
` |
||||
@ -0,0 +1,34 @@ |
||||
import styled from 'styled-components/macro' |
||||
|
||||
import type { Tournament } from 'requests/getSportTournaments' |
||||
|
||||
import { |
||||
ItemInfo, |
||||
Name, |
||||
Flag, |
||||
TeamOrCountry, |
||||
} from 'features/ItemsList/styled' |
||||
import { SportIcon } from 'features/SportIcon' |
||||
|
||||
const Wrapper = styled.div` |
||||
width: 100%; |
||||
max-width: 300px; |
||||
display: flex; |
||||
flex-direction: column; |
||||
justify-content: center; |
||||
` |
||||
|
||||
type Props = { |
||||
tournament: Tournament, |
||||
} |
||||
|
||||
export const TournamentInfo = ({ tournament }: Props) => ( |
||||
<Wrapper> |
||||
<Name nameObj={tournament} /> |
||||
<ItemInfo> |
||||
<SportIcon sport={tournament.sport} /> |
||||
<Flag src={`https://instatscout.com/images/flags/48/${tournament.country.id}.png`} /> |
||||
<TeamOrCountry nameObj={tournament.country} /> |
||||
</ItemInfo> |
||||
</Wrapper> |
||||
) |
||||
@ -0,0 +1,59 @@ |
||||
import type { CSSProperties } from 'react' |
||||
import type { ListChildComponentProps } from 'react-window' |
||||
import styled from 'styled-components/macro' |
||||
|
||||
import floor from 'lodash/floor' |
||||
|
||||
import type { Tournament, Tournaments } from 'requests' |
||||
|
||||
import { TournamentInfo } from '../TournamentInfo' |
||||
import { Checkbox } from '../../styled' |
||||
|
||||
const Item = styled.li` |
||||
width: 48.5%; |
||||
padding: 6.5px 0; |
||||
` |
||||
|
||||
const getTwoColumnListStyles = (index: number) => { |
||||
const isEven = index % 2 === 0 |
||||
const rowIndex = floor(index / 2) |
||||
const style: CSSProperties = { |
||||
position: 'absolute', |
||||
top: 48 * rowIndex, |
||||
} |
||||
if (isEven) { |
||||
style.left = 0 |
||||
} else { |
||||
style.right = 0 |
||||
} |
||||
return style |
||||
} |
||||
|
||||
export type ItemData = { |
||||
isTournamentSelected: (tournament: Tournament) => boolean, |
||||
onTournamentSelect: (tournament: Tournament) => void, |
||||
tournaments: Tournaments, |
||||
} |
||||
|
||||
export const TournamentListItem = ({ |
||||
data, |
||||
index, |
||||
}: ListChildComponentProps<ItemData>) => { |
||||
const { |
||||
isTournamentSelected, |
||||
onTournamentSelect, |
||||
tournaments, |
||||
} = data |
||||
const tournament = tournaments[index] |
||||
if (!tournament) return null |
||||
|
||||
return ( |
||||
<Item style={getTwoColumnListStyles(index)}> |
||||
<Checkbox |
||||
checked={isTournamentSelected(tournament)} |
||||
label={<TournamentInfo tournament={tournament} />} |
||||
onChange={() => onTournamentSelect(tournament)} |
||||
/> |
||||
</Item> |
||||
) |
||||
} |
||||
@ -0,0 +1,53 @@ |
||||
import styled from 'styled-components/macro' |
||||
|
||||
import { T9n } from 'features/T9n' |
||||
|
||||
import { usePreferencesStore } from '../../store' |
||||
import { Search } from '../Search' |
||||
import { TournamentsList } from '../TournamentsList' |
||||
|
||||
import { BlockTitle, Checkbox } from '../../styled' |
||||
|
||||
const Wrapper = styled.div` |
||||
display: flex; |
||||
flex-direction: column; |
||||
align-items: flex-start; |
||||
padding-left: 14px; |
||||
flex-grow: 1; |
||||
` |
||||
|
||||
const CheckboxWrapper = styled.div` |
||||
margin: 20px 0 22px 2px; |
||||
margin-top: 20px; |
||||
margin-bottom: 22px; |
||||
` |
||||
|
||||
export const TournamentsBlock = () => { |
||||
const { |
||||
allTournamentsSelected, |
||||
isTournamentSelected, |
||||
onSelectAllTournaments, |
||||
onTournamentSelect, |
||||
tournaments, |
||||
} = usePreferencesStore() |
||||
return ( |
||||
<Wrapper> |
||||
<BlockTitle> |
||||
<T9n t='tournament' /> |
||||
</BlockTitle> |
||||
<CheckboxWrapper> |
||||
<Checkbox |
||||
labelLexic='all' |
||||
checked={allTournamentsSelected} |
||||
onChange={onSelectAllTournaments} |
||||
/> |
||||
</CheckboxWrapper> |
||||
<Search /> |
||||
<TournamentsList |
||||
isTournamentSelected={isTournamentSelected} |
||||
onTournamentSelect={onTournamentSelect} |
||||
tournaments={tournaments} |
||||
/> |
||||
</Wrapper> |
||||
) |
||||
} |
||||
@ -0,0 +1,68 @@ |
||||
import { memo } from 'react' |
||||
import type { ListItemKeySelector } from 'react-window' |
||||
import { FixedSizeList } from 'react-window' |
||||
|
||||
import styled from 'styled-components/macro' |
||||
|
||||
import size from 'lodash/size' |
||||
|
||||
import { customScrollbar } from 'features/Common' |
||||
|
||||
import type { ItemData } from '../TournamentListItem' |
||||
import { TournamentListItem } from '../TournamentListItem' |
||||
|
||||
const getItemKey: ListItemKeySelector = (index, data) => { |
||||
const tournament = data[index] |
||||
|
||||
return tournament |
||||
? `${tournament.sport}_${tournament.id}` |
||||
: index |
||||
} |
||||
|
||||
type Props = ItemData & { |
||||
className?: string, |
||||
} |
||||
|
||||
const List = memo((props: Props) => { |
||||
const { className, tournaments } = props |
||||
return ( |
||||
<FixedSizeList |
||||
className={className} |
||||
innerElementType='ul' |
||||
width='100%' |
||||
height={432} |
||||
itemSize={24} |
||||
itemCount={size(tournaments)} |
||||
itemKey={getItemKey} |
||||
itemData={props} |
||||
> |
||||
{TournamentListItem} |
||||
</FixedSizeList> |
||||
) |
||||
}) |
||||
|
||||
export const TournamentsList = styled(List)` |
||||
width: 100%; |
||||
height: 432px; |
||||
margin-top: 14px; |
||||
display: flex; |
||||
flex-wrap: wrap; |
||||
justify-content: space-between; |
||||
align-content: flex-start; |
||||
overflow-y: auto; |
||||
|
||||
@media (max-width: 1370px) { |
||||
height: 20rem !important; |
||||
} |
||||
|
||||
${customScrollbar} |
||||
|
||||
::-webkit-scrollbar { |
||||
width: 5px; |
||||
height: 5px; |
||||
} |
||||
|
||||
::-webkit-scrollbar-thumb { |
||||
background: rgba(255, 255, 255, 0.2); |
||||
} |
||||
` |
||||
@ -0,0 +1,61 @@ |
||||
import { |
||||
HeaderActions, |
||||
CloseButton, |
||||
} from 'features/PopupComponents' |
||||
import { T9n } from 'features/T9n' |
||||
|
||||
import { SportsList } from './components/SportList' |
||||
import { TournamentsBlock } from './components/TournamentsBlock' |
||||
import { usePreferencesStore } from './store' |
||||
|
||||
import { |
||||
Modal, |
||||
Wrapper, |
||||
Header, |
||||
HeaderTitle, |
||||
Body, |
||||
Footer, |
||||
ApplyButton, |
||||
Loader, |
||||
} from './styled' |
||||
|
||||
export * from './store' |
||||
|
||||
export const PreferencesPopup = () => { |
||||
const { |
||||
closePopup, |
||||
isApplyButtonDisabled, |
||||
isFetching, |
||||
isOpen, |
||||
onApplyClick, |
||||
} = usePreferencesStore() |
||||
|
||||
return ( |
||||
<Modal |
||||
isOpen={isOpen} |
||||
close={closePopup} |
||||
withCloseButton={false} |
||||
> |
||||
{isFetching && <Loader />} |
||||
<Wrapper isFetching={isFetching}> |
||||
<Header> |
||||
<HeaderTitle> |
||||
<T9n t='my_preferences' /> |
||||
</HeaderTitle> |
||||
<HeaderActions position='right'> |
||||
<CloseButton onClick={closePopup} /> |
||||
</HeaderActions> |
||||
</Header> |
||||
<Body> |
||||
<SportsList /> |
||||
<TournamentsBlock /> |
||||
</Body> |
||||
<Footer> |
||||
<ApplyButton onClick={onApplyClick} disabled={isApplyButtonDisabled}> |
||||
<T9n t='apply' /> |
||||
</ApplyButton> |
||||
</Footer> |
||||
</Wrapper> |
||||
</Modal> |
||||
) |
||||
} |
||||
@ -0,0 +1,50 @@ |
||||
import includes from 'lodash/includes' |
||||
import toLower from 'lodash/toLower' |
||||
import reduce from 'lodash/reduce' |
||||
import filter from 'lodash/filter' |
||||
|
||||
import type { SportTypes } from 'config' |
||||
import type { TournamentsBySports, Tournaments } from 'requests/getSportTournaments' |
||||
|
||||
const getFlatTournaments = ( |
||||
tournamentsBySports: TournamentsBySports, |
||||
selectedSports: Array<SportTypes>, |
||||
) => ( |
||||
reduce( |
||||
selectedSports, |
||||
(acc, sport) => { |
||||
const sportTournaments = tournamentsBySports[sport] |
||||
if (sportTournaments) { |
||||
return [...acc, ...sportTournaments] |
||||
} |
||||
return acc |
||||
}, |
||||
[] as Tournaments, |
||||
) |
||||
) |
||||
|
||||
type NameKey = 'name_eng' | 'name_rus' |
||||
|
||||
type SearchProps = { |
||||
query: string, |
||||
selectedSports: Array<SportTypes>, |
||||
suffix: string, |
||||
tournamentsBySports: TournamentsBySports, |
||||
} |
||||
|
||||
export const search = async ({ |
||||
query, |
||||
selectedSports, |
||||
suffix, |
||||
tournamentsBySports, |
||||
}: SearchProps) => { |
||||
const tournaments = getFlatTournaments(tournamentsBySports, selectedSports) |
||||
const nameKey = `name_${suffix}` as NameKey |
||||
return filter( |
||||
tournaments, |
||||
(tournament) => includes( |
||||
toLower(tournament[nameKey]), |
||||
toLower(query), |
||||
), |
||||
) |
||||
} |
||||
@ -0,0 +1,124 @@ |
||||
import { useEffect, useCallback } from 'react' |
||||
|
||||
import isBoolean from 'lodash/isBoolean' |
||||
import uniq from 'lodash/uniq' |
||||
import map from 'lodash/map' |
||||
|
||||
import { useLocalStore, useToggle } from 'hooks' |
||||
|
||||
import { useTournamentsBySports } from './useTournamentsBySports' |
||||
import { usePreferencesLoader } from './usePreferencesLoader' |
||||
import { useSelectedTournaments } from './useTournaments' |
||||
import { useSports } from './useSports' |
||||
import { useSearch } from './useSearch' |
||||
|
||||
export const usePreferences = () => { |
||||
const [firstLoad, setFirstLoad] = useLocalStore({ |
||||
// defaultValue: true,
|
||||
defaultValue: false, |
||||
key: 'preferences_first_load', |
||||
validator: isBoolean, |
||||
}) |
||||
const { |
||||
close, |
||||
isOpen, |
||||
// open,
|
||||
} = useToggle() |
||||
|
||||
const { |
||||
// fetchInitialSports,
|
||||
onSportSelect, |
||||
selectedSports, |
||||
setInitialSelectedSports, |
||||
sports, |
||||
} = useSports() |
||||
|
||||
const { |
||||
isFetching: isFetchingTournaments, |
||||
tournamentsBySports, |
||||
} = useTournamentsBySports(selectedSports) |
||||
|
||||
const { |
||||
clearQuery, |
||||
onQueryChange, |
||||
query, |
||||
tournaments, |
||||
} = useSearch(tournamentsBySports, selectedSports) |
||||
|
||||
const { |
||||
allTournamentsSelected, |
||||
isTournamentSelected, |
||||
onSelectAllTournaments, |
||||
onTournamentSelect, |
||||
selectedTournaments, |
||||
setInitialSelectedTournaments, |
||||
} = useSelectedTournaments(selectedSports, tournaments) |
||||
|
||||
const { |
||||
// fetchInitialPreferences,
|
||||
isApplyButtonDisabled, |
||||
isFetching: isFetchingPrefernces, |
||||
isSaving, |
||||
onApplyClick, |
||||
userPreferences, |
||||
} = usePreferencesLoader(selectedTournaments, close) |
||||
|
||||
const reset = useCallback(() => { |
||||
if (!userPreferences) return |
||||
|
||||
const initialSelectedSports = uniq(map( |
||||
userPreferences, |
||||
({ sport }) => sport, |
||||
)) |
||||
setInitialSelectedTournaments(userPreferences) |
||||
setInitialSelectedSports(initialSelectedSports) |
||||
}, [ |
||||
userPreferences, |
||||
setInitialSelectedTournaments, |
||||
setInitialSelectedSports, |
||||
]) |
||||
|
||||
const openPopup = useCallback(() => { |
||||
// fetchInitialPreferences()
|
||||
// fetchInitialSports()
|
||||
// open()
|
||||
}, [/* fetchInitialPreferences, fetchInitialSports, open */]) |
||||
|
||||
const closePopup = () => { |
||||
reset() |
||||
close() |
||||
} |
||||
|
||||
useEffect(reset, [reset]) |
||||
useEffect(() => { |
||||
if (firstLoad) { |
||||
openPopup() |
||||
setFirstLoad(false) |
||||
} |
||||
}, [ |
||||
firstLoad, |
||||
openPopup, |
||||
setFirstLoad, |
||||
]) |
||||
|
||||
return { |
||||
allTournamentsSelected, |
||||
clearQuery, |
||||
closePopup, |
||||
isApplyButtonDisabled, |
||||
isFetching: isFetchingPrefernces || isSaving || isFetchingTournaments, |
||||
isOpen, |
||||
isTournamentSelected, |
||||
onApplyClick, |
||||
onQueryChange, |
||||
onSelectAllTournaments, |
||||
onSportSelect, |
||||
onTournamentSelect, |
||||
openPopup, |
||||
query, |
||||
selectedSports, |
||||
sports, |
||||
tournaments, |
||||
userPreferences, |
||||
} |
||||
} |
||||
@ -0,0 +1,68 @@ |
||||
import { |
||||
useCallback, |
||||
useState, |
||||
useMemo, |
||||
} from 'react' |
||||
|
||||
import isEmpty from 'lodash/isEmpty' |
||||
import isNull from 'lodash/isNull' |
||||
import size from 'lodash/size' |
||||
import xorWith from 'lodash/xorWith' |
||||
import isEqual from 'lodash/isEqual' |
||||
|
||||
import type{ UserPreferences } from 'requests/getUserPreferences' |
||||
import { getUserPreferences as getUserPreferencesRequest } from 'requests/getUserPreferences' |
||||
import { saveUserPreferences as saveUserPreferencesRequest } from 'requests/saveUserPreferences' |
||||
|
||||
import { useRequest } from 'hooks' |
||||
|
||||
export const usePreferencesLoader = ( |
||||
selectedTournaments: UserPreferences, |
||||
onApply: () => void, |
||||
) => { |
||||
const [userPreferences, setUserPreferences] = useState<UserPreferences | null>(null) |
||||
|
||||
const { |
||||
isFetching, |
||||
request: fetchUserPreferences, |
||||
} = useRequest(getUserPreferencesRequest) |
||||
const { |
||||
isFetching: isSaving, |
||||
request: saveUserPreferences, |
||||
} = useRequest(saveUserPreferencesRequest) |
||||
|
||||
const fetchInitialPreferences = useCallback(() => { |
||||
if (isNull(userPreferences)) { |
||||
fetchUserPreferences().then(setUserPreferences) |
||||
} |
||||
}, [fetchUserPreferences, userPreferences]) |
||||
|
||||
const onApplyClick = () => { |
||||
const data = isEmpty(selectedTournaments) ? null : selectedTournaments |
||||
saveUserPreferences(data) |
||||
.then(fetchUserPreferences) |
||||
.then(setUserPreferences) |
||||
.then(onApply) |
||||
} |
||||
|
||||
const isApplyButtonDisabled = useMemo(() => { |
||||
if (!userPreferences) return true |
||||
|
||||
if (size(userPreferences) !== size(selectedTournaments)) return false |
||||
|
||||
return isEmpty(xorWith( |
||||
userPreferences, |
||||
selectedTournaments, |
||||
isEqual, |
||||
)) |
||||
}, [selectedTournaments, userPreferences]) |
||||
|
||||
return { |
||||
fetchInitialPreferences, |
||||
isApplyButtonDisabled, |
||||
isFetching, |
||||
isSaving, |
||||
onApplyClick, |
||||
userPreferences, |
||||
} |
||||
} |
||||
@ -0,0 +1,62 @@ |
||||
import type { ChangeEvent, MouseEvent } from 'react' |
||||
import { |
||||
useState, |
||||
useEffect, |
||||
useMemo, |
||||
} from 'react' |
||||
|
||||
import debounce from 'lodash/debounce' |
||||
|
||||
import type { SportTypes } from 'config' |
||||
import type { Tournaments, TournamentsBySports } from 'requests/getSportTournaments' |
||||
|
||||
import { useLexicsStore } from 'features/LexicsStore' |
||||
|
||||
import { search } from '../helpers' |
||||
|
||||
type SearchArgs = Parameters<typeof search>[0] |
||||
|
||||
export const useSearch = ( |
||||
tournamentsBySports: TournamentsBySports, |
||||
selectedSports: Array<SportTypes>, |
||||
) => { |
||||
const { suffix } = useLexicsStore() |
||||
|
||||
const [query, setQuery] = useState('') |
||||
const [tournaments, setTournaments] = useState<Tournaments>([]) |
||||
|
||||
const onQueryChange = (e: ChangeEvent<HTMLInputElement>) => { |
||||
setQuery(e.target.value) |
||||
} |
||||
|
||||
const clearQuery = (e: MouseEvent) => { |
||||
e.stopPropagation() |
||||
setQuery('') |
||||
} |
||||
|
||||
const debouncedSearch = useMemo(() => debounce((args: SearchArgs) => { |
||||
search(args).then(setTournaments) |
||||
}, 300), []) |
||||
|
||||
useEffect(() => { |
||||
debouncedSearch({ |
||||
query, |
||||
selectedSports, |
||||
suffix, |
||||
tournamentsBySports, |
||||
}) |
||||
}, [ |
||||
selectedSports, |
||||
query, |
||||
tournamentsBySports, |
||||
suffix, |
||||
debouncedSearch, |
||||
]) |
||||
|
||||
return { |
||||
clearQuery, |
||||
onQueryChange, |
||||
query, |
||||
tournaments, |
||||
} |
||||
} |
||||
@ -0,0 +1,35 @@ |
||||
import { useCallback, useState } from 'react' |
||||
|
||||
import includes from 'lodash/includes' |
||||
import without from 'lodash/without' |
||||
import isEmpty from 'lodash/isEmpty' |
||||
|
||||
import type { SportTypes } from 'config' |
||||
import type { SportList } from 'requests/getSportList' |
||||
import { getSportList } from 'requests/getSportList' |
||||
|
||||
export const useSports = () => { |
||||
const [sports, setSports] = useState<SportList>([]) |
||||
const [selectedSports, setSelectedSports] = useState<Array<SportTypes>>([]) |
||||
|
||||
const fetchInitialSports = useCallback(() => { |
||||
if (isEmpty(sports)) { |
||||
getSportList().then(setSports) |
||||
} |
||||
}, [sports]) |
||||
|
||||
const onSportSelect = (sport: SportTypes) => { |
||||
const newSports = includes(selectedSports, sport) |
||||
? without(selectedSports, sport) |
||||
: [sport, ...selectedSports] |
||||
setSelectedSports(newSports) |
||||
} |
||||
|
||||
return { |
||||
fetchInitialSports, |
||||
onSportSelect, |
||||
selectedSports, |
||||
setInitialSelectedSports: setSelectedSports, |
||||
sports, |
||||
} |
||||
} |
||||
@ -0,0 +1,77 @@ |
||||
import { |
||||
useMemo, |
||||
useState, |
||||
useCallback, |
||||
useEffect, |
||||
} from 'react' |
||||
|
||||
import includes from 'lodash/includes' |
||||
import isEmpty from 'lodash/isEmpty' |
||||
import filter from 'lodash/filter' |
||||
import remove from 'lodash/remove' |
||||
import every from 'lodash/every' |
||||
import find from 'lodash/find' |
||||
import map from 'lodash/map' |
||||
|
||||
import type { SportTypes } from 'config' |
||||
|
||||
import type { Tournament, Tournaments } from 'requests/getSportTournaments' |
||||
import type { UserPreferences } from 'requests/getUserPreferences' |
||||
|
||||
export const useSelectedTournaments = ( |
||||
selectedSports: Array<SportTypes>, |
||||
tournaments: Tournaments, |
||||
) => { |
||||
const [selectedTournaments, setSelectedTournaments] = useState<UserPreferences>([]) |
||||
|
||||
const isTournamentSelected = useCallback(({ id, sport }: Tournament) => ( |
||||
Boolean(find( |
||||
selectedTournaments, |
||||
{ sport, tournament_id: id }, |
||||
)) |
||||
), [selectedTournaments]) |
||||
|
||||
const onTournamentSelect = useCallback((tournament: Tournament) => { |
||||
const preference = { sport: tournament.sport, tournament_id: tournament.id } |
||||
if (isTournamentSelected(tournament)) { |
||||
remove(selectedTournaments, preference) |
||||
setSelectedTournaments([...selectedTournaments]) |
||||
} else { |
||||
setSelectedTournaments([...selectedTournaments, preference]) |
||||
} |
||||
}, [isTournamentSelected, selectedTournaments]) |
||||
|
||||
const allTournamentsSelected = useMemo( |
||||
() => { |
||||
if (isEmpty(tournaments)) return false |
||||
|
||||
return every(tournaments, isTournamentSelected) |
||||
}, |
||||
[isTournamentSelected, tournaments], |
||||
) |
||||
|
||||
const onSelectAllTournaments = () => { |
||||
const newSelectedTournaments = allTournamentsSelected |
||||
? [] |
||||
: map( |
||||
tournaments, |
||||
({ id, sport }) => ({ sport, tournament_id: id }), |
||||
) |
||||
setSelectedTournaments(newSelectedTournaments) |
||||
} |
||||
|
||||
useEffect(() => { |
||||
setSelectedTournaments((oldTournaments) => ( |
||||
filter(oldTournaments, ({ sport }) => includes(selectedSports, sport)) |
||||
)) |
||||
}, [selectedSports]) |
||||
|
||||
return { |
||||
allTournamentsSelected, |
||||
isTournamentSelected, |
||||
onSelectAllTournaments, |
||||
onTournamentSelect, |
||||
selectedTournaments, |
||||
setInitialSelectedTournaments: setSelectedTournaments, |
||||
} |
||||
} |
||||
@ -0,0 +1,37 @@ |
||||
import { useState, useEffect } from 'react' |
||||
|
||||
import isEmpty from 'lodash/isEmpty' |
||||
import filter from 'lodash/filter' |
||||
|
||||
import type { SportTypes } from 'config' |
||||
|
||||
import type { TournamentsBySports } from 'requests/getSportTournaments' |
||||
import { getTournamentsBySports } from 'requests/getSportTournaments' |
||||
|
||||
import { useRequest } from 'hooks' |
||||
|
||||
export const useTournamentsBySports = (selectedSports: Array<SportTypes>) => { |
||||
const [tournamentsBySports, setTournamentsBySports] = useState<TournamentsBySports>({}) |
||||
|
||||
const { |
||||
isFetching, |
||||
request: fetchTournaments, |
||||
} = useRequest(getTournamentsBySports) |
||||
|
||||
useEffect(() => { |
||||
const missingTournaments = filter(selectedSports, (sport) => !tournamentsBySports[sport]) |
||||
if (!isEmpty(missingTournaments)) { |
||||
fetchTournaments(missingTournaments).then((newTournaments) => { |
||||
setTournamentsBySports({ |
||||
...tournamentsBySports, |
||||
...newTournaments, |
||||
}) |
||||
}) |
||||
} |
||||
}, [selectedSports, tournamentsBySports, fetchTournaments]) |
||||
|
||||
return { |
||||
isFetching, |
||||
tournamentsBySports, |
||||
} |
||||
} |
||||
@ -0,0 +1,20 @@ |
||||
import type { ReactNode } from 'react' |
||||
import { createContext, useContext } from 'react' |
||||
|
||||
import { usePreferences } from './hooks' |
||||
|
||||
type Context = ReturnType<typeof usePreferences> |
||||
type Props = { children: ReactNode } |
||||
|
||||
const PreferencesPopupContext = createContext({} as Context) |
||||
|
||||
export const PreferencesPopupStore = ({ children }: Props) => { |
||||
const value = usePreferences() |
||||
return ( |
||||
<PreferencesPopupContext.Provider value={value}> |
||||
{children} |
||||
</PreferencesPopupContext.Provider> |
||||
) |
||||
} |
||||
|
||||
export const usePreferencesStore = () => useContext(PreferencesPopupContext) |
||||
@ -0,0 +1,106 @@ |
||||
import styled, { css } from 'styled-components/macro' |
||||
|
||||
import { ModalWindow } from 'features/Modal/styled' |
||||
import { Modal as BaseModal } from 'features/Modal' |
||||
import { Header as BaseHeader } from 'features/PopupComponents' |
||||
|
||||
import { ButtonSolid } from 'features/Common' |
||||
import { Checkbox as BaseCheckbox } from 'features/Common/Checkbox' |
||||
import { Label } from 'features/Common/Checkbox/styled' |
||||
import { CheckboxSvg } from 'features/Common/Checkbox/Icon' |
||||
import { ArrowLoader } from 'features/ArrowLoader' |
||||
|
||||
export const Modal = styled(BaseModal)` |
||||
background-color: rgba(0, 0, 0, 0.7); |
||||
padding: 0 60px; |
||||
|
||||
${ModalWindow} { |
||||
width: 1066px; |
||||
height: 781px; |
||||
padding: 0; |
||||
background-color: #333333; |
||||
border-radius: 5px; |
||||
|
||||
@media (max-width: 1370px) { |
||||
width: 70rem; |
||||
height: auto; |
||||
} |
||||
} |
||||
` |
||||
|
||||
type WrapperProps = { |
||||
isFetching?: boolean, |
||||
} |
||||
|
||||
export const Wrapper = styled.div<WrapperProps>` |
||||
${({ isFetching }) => ( |
||||
isFetching |
||||
? css`pointer-events: none;` |
||||
: '' |
||||
)} |
||||
` |
||||
|
||||
export const Header = styled(BaseHeader)` |
||||
height: auto; |
||||
padding-top: 40px; |
||||
justify-content: center; |
||||
` |
||||
|
||||
export const HeaderTitle = styled.span` |
||||
font-weight: 600; |
||||
font-size: 1.13rem; |
||||
line-height: 1.13rem; |
||||
color: #FFFFFF; |
||||
` |
||||
|
||||
export const BlockTitle = styled.span` |
||||
font-weight: 600; |
||||
font-size: 0.56rem; |
||||
line-height: 0.85rem; |
||||
text-transform: uppercase; |
||||
color: rgba(255, 255, 255, 0.5); |
||||
` |
||||
|
||||
export const Body = styled.div` |
||||
padding: 25px 40px 0 42px; |
||||
display: flex; |
||||
` |
||||
|
||||
export const Footer = styled.div` |
||||
width: 100%; |
||||
display: flex; |
||||
justify-content: center; |
||||
padding: 1.89rem; |
||||
` |
||||
|
||||
export const Checkbox = styled(BaseCheckbox)` |
||||
${Label} { |
||||
font-weight: 600; |
||||
font-size: 0.66rem; |
||||
line-height: 0.94rem; |
||||
letter-spacing: 0.1px; |
||||
} |
||||
${CheckboxSvg} { |
||||
align-self: auto; |
||||
margin-right: 18px; |
||||
max-width: 20px; |
||||
max-height: 20px; |
||||
} |
||||
` |
||||
|
||||
export const ApplyButton = styled(ButtonSolid)` |
||||
width: 270px; |
||||
border-radius: 5px; |
||||
` |
||||
|
||||
export const Loader = styled(ArrowLoader)` |
||||
position: absolute; |
||||
z-index: 10; |
||||
background-color: #3f3f3f60; |
||||
width: 100%; |
||||
height: 100%; |
||||
display: flex; |
||||
align-items: center; |
||||
justify-content: center; |
||||
pointer-events: none; |
||||
` |
||||
@ -0,0 +1,29 @@ |
||||
import { |
||||
DATA_URL, |
||||
PROCEDURES, |
||||
SportTypes, |
||||
} from 'config' |
||||
import { callApi } from 'helpers' |
||||
|
||||
const proc = PROCEDURES.get_user_preferences |
||||
|
||||
export type UserPreferences = Array<{ |
||||
sport: SportTypes, |
||||
tournament_id: number, |
||||
}> |
||||
|
||||
export const getUserPreferences = async () => { |
||||
const config = { |
||||
body: { |
||||
params: {}, |
||||
proc, |
||||
}, |
||||
} |
||||
|
||||
const response: UserPreferences | null = await callApi({ |
||||
config, |
||||
url: DATA_URL, |
||||
}) |
||||
|
||||
return response || [] |
||||
} |
||||
@ -0,0 +1,25 @@ |
||||
import { |
||||
DATA_URL, |
||||
PROCEDURES, |
||||
} from 'config' |
||||
import { callApi } from 'helpers' |
||||
|
||||
import type { UserPreferences } from './getUserPreferences' |
||||
|
||||
const proc = PROCEDURES.save_user_preferences |
||||
|
||||
export const saveUserPreferences = (data: UserPreferences | null) => { |
||||
const config = { |
||||
body: { |
||||
params: { |
||||
_p_data: data, |
||||
}, |
||||
proc, |
||||
}, |
||||
} |
||||
|
||||
return callApi({ |
||||
config, |
||||
url: DATA_URL, |
||||
}) |
||||
} |
||||
Loading…
Reference in new issue