fix(#720): timeline mode
parent
af912dca10
commit
c41acef4ce
@ -1,193 +0,0 @@ |
||||
import map from 'lodash/map' |
||||
|
||||
import { T9n } from 'features/T9n' |
||||
import { Icon } from 'features/Icon' |
||||
import { OutsideClick } from 'features/OutsideClick' |
||||
import { BodyBackdrop } from 'features/PageLayout' |
||||
import { useHeaderFiltersStore } from 'features/HeaderFilters' |
||||
|
||||
import { Fragment } from 'react' |
||||
import { useDateFilter } from '../DateFilter/hooks' |
||||
import { DatePicker } from '../DatePicker' |
||||
import { Tabs } from '../../store/config' |
||||
|
||||
import { |
||||
TabsList, |
||||
Tab, |
||||
TabTitle, |
||||
MonthsMode, |
||||
YearWrapper, |
||||
ArrowButton, |
||||
Arrow, |
||||
MonthModeYear, |
||||
MonthModeWrapper, |
||||
Months, |
||||
Month, |
||||
MonthName, |
||||
FacrWrapper, |
||||
DateWrapper, |
||||
MonthArrow, |
||||
WeekDaysWrapper, |
||||
FacrMonthWrapper, |
||||
WeekDay, |
||||
WeekName, |
||||
WeekNumber, |
||||
FacrDateButton, |
||||
FacrWeek, |
||||
} from '../DateFilter/styled' |
||||
|
||||
export const FacrDateFilter = () => { |
||||
const { |
||||
addAdsViews, |
||||
close, |
||||
date, |
||||
isMonthMode, |
||||
isOpen, |
||||
months, |
||||
onDateChange, |
||||
onNextClick, |
||||
onNextYearClick, |
||||
onPreviousClick, |
||||
onPrevYearClick, |
||||
onWeekDayClick, |
||||
openDatePicker, |
||||
selectedDate, |
||||
selectedMode, |
||||
selectedMonthModeDate, |
||||
setSelectedMode, |
||||
setSelectedMonthModeDate, |
||||
week, |
||||
} = useDateFilter() |
||||
|
||||
const { |
||||
resetFilters, |
||||
} = useHeaderFiltersStore() |
||||
|
||||
return ( |
||||
<FacrWrapper isMonthMode={isMonthMode}> |
||||
<DateWrapper isMonthMode={isMonthMode}> |
||||
<TabsList> |
||||
<Tab |
||||
aria-pressed={selectedMode === Tabs.MONTH} |
||||
onClick={() => setSelectedMode(Tabs.MONTH)} |
||||
> |
||||
<TabTitle> |
||||
<T9n t='month_title' /> |
||||
</TabTitle> |
||||
</Tab> |
||||
<Tab |
||||
aria-pressed={selectedMode === Tabs.WEEK} |
||||
onClick={() => setSelectedMode(Tabs.WEEK)} |
||||
> |
||||
<TabTitle> |
||||
<T9n t='week_title' /> |
||||
</TabTitle> |
||||
</Tab> |
||||
</TabsList> |
||||
{isMonthMode |
||||
? ( |
||||
<YearWrapper> |
||||
<MonthArrow |
||||
aria-label='Previous year' |
||||
onClick={onPrevYearClick} |
||||
> |
||||
<Arrow direction='left' /> |
||||
</MonthArrow> |
||||
<MonthModeYear> |
||||
{selectedMonthModeDate.getFullYear()} |
||||
</MonthModeYear> |
||||
<MonthArrow |
||||
aria-label='Next year' |
||||
onClick={onNextYearClick} |
||||
> |
||||
<Arrow direction='right' /> |
||||
</MonthArrow> |
||||
</YearWrapper> |
||||
) |
||||
: ( |
||||
<FacrMonthWrapper> |
||||
<MonthModeYear onClick={openDatePicker}> |
||||
{date.month} {' '} {date.year} |
||||
</MonthModeYear> |
||||
<FacrDateButton isActive={isOpen} onClick={openDatePicker}> |
||||
<Icon refIcon='Calendar' color='#fff' /> |
||||
</FacrDateButton> |
||||
</FacrMonthWrapper> |
||||
)} |
||||
</DateWrapper> |
||||
|
||||
{isMonthMode |
||||
? ( |
||||
<MonthsMode> |
||||
<MonthModeWrapper isMonthMode={isMonthMode}> |
||||
<Months> |
||||
{ |
||||
map(months, (day) => ( |
||||
<Month |
||||
key={day.name} |
||||
selected={day.date.getMonth() === selectedMonthModeDate.getMonth()} |
||||
onClick={() => setSelectedMonthModeDate(day.date)} |
||||
> |
||||
<MonthName>{day.name}</MonthName> |
||||
</Month> |
||||
)) |
||||
} |
||||
</Months> |
||||
</MonthModeWrapper> |
||||
</MonthsMode> |
||||
) |
||||
: ( |
||||
<WeekDaysWrapper> |
||||
<ArrowButton |
||||
aria-label='Previous week' |
||||
onClick={onPreviousClick} |
||||
> |
||||
<Arrow direction='left' /> |
||||
</ArrowButton> |
||||
<FacrWeek> |
||||
{ |
||||
map(week, (day) => ( |
||||
<WeekDay |
||||
key={day.name} |
||||
selected={day.date.getDate() === selectedDate.getDate()} |
||||
onClick={() => { |
||||
if (day.date.getDate() !== selectedDate.getDate()) { |
||||
addAdsViews() |
||||
onWeekDayClick(day.date) |
||||
} else { |
||||
resetFilters() |
||||
} |
||||
}} |
||||
> |
||||
<WeekName>{day.name.slice(0, 3)}</WeekName> |
||||
<WeekNumber>{day.date.getDate()}</WeekNumber> |
||||
</WeekDay> |
||||
)) |
||||
} |
||||
</FacrWeek> |
||||
<ArrowButton |
||||
aria-label='Next week' |
||||
onClick={onNextClick} |
||||
> |
||||
<Arrow direction='right' /> |
||||
</ArrowButton> |
||||
</WeekDaysWrapper> |
||||
|
||||
)} |
||||
{ |
||||
isOpen && ( |
||||
<Fragment> |
||||
<OutsideClick onClick={close}> |
||||
<DatePicker |
||||
open |
||||
selected={selectedDate} |
||||
onChange={onDateChange} |
||||
/> |
||||
</OutsideClick> |
||||
<BodyBackdrop /> |
||||
</Fragment> |
||||
) |
||||
} |
||||
</FacrWrapper> |
||||
) |
||||
} |
||||
@ -1,3 +1,2 @@ |
||||
export * from './components/DateFilter' |
||||
export * from './components/FacrDateFilter' |
||||
export * from './store' |
||||
|
||||
@ -1,3 +1,3 @@ |
||||
export const MATCH_CARD_WIDTH = 12 |
||||
export const MATCH_CARD_WIDTH = 22 |
||||
|
||||
export const MATCH_CARD_GAP = 20 |
||||
export const MATCH_CARD_GAP = 40 |
||||
|
||||
@ -1,53 +1,111 @@ |
||||
import styled from 'styled-components/macro' |
||||
import styled, { css } from 'styled-components/macro' |
||||
|
||||
import { devices } from 'config/devices' |
||||
|
||||
import { CardWrapper } from '../MatchCard/styled' |
||||
import { CardWrapper, CardWrapperOuter } from '../MatchCard/styled' |
||||
|
||||
export const Wrapper = styled.div` |
||||
position: relative; |
||||
margin-bottom: 16px; |
||||
overflow: hidden; |
||||
padding-right: 5px; |
||||
` |
||||
|
||||
export const Slides = styled.ul` |
||||
display: flex; |
||||
scroll-behavior: smooth; |
||||
overflow-x: auto; |
||||
gap: 0.9rem; |
||||
padding: 10px 10px 10px 7px; |
||||
|
||||
&::-webkit-scrollbar { |
||||
display: none; |
||||
} |
||||
scrollbar-width: none;
|
||||
|
||||
${CardWrapperOuter} { |
||||
padding-top: 0; |
||||
} |
||||
|
||||
${CardWrapper} { |
||||
width: 283px; |
||||
position: relative; |
||||
height: 12.9rem; |
||||
width: 12.9rem; |
||||
|
||||
&:hover { |
||||
scale: 1.04; |
||||
} |
||||
|
||||
@media ${devices.laptop} { |
||||
min-width: auto; |
||||
width: 279px; |
||||
@media screen and (min-width: 1920px) { |
||||
height: 13.8rem; |
||||
width: 13.8rem; |
||||
} |
||||
} |
||||
|
||||
@media ${devices.mobile} { |
||||
flex-direction: column; |
||||
@media screen and (max-width: 1920px) { |
||||
height: 13.4rem; |
||||
width: 13.4rem; |
||||
} |
||||
|
||||
${CardWrapper} { |
||||
width: 100%; |
||||
} |
||||
@media screen and (max-width: 1440px) { |
||||
height: 12.8rem; |
||||
width: 12.8rem; |
||||
} |
||||
|
||||
@media screen and (max-width: 1280px) { |
||||
height: 12.6rem; |
||||
width: 12.6rem; |
||||
} |
||||
} |
||||
` |
||||
|
||||
export const Arrow = styled.div<{ type: 'arrowLeft' | 'arrowRight' }>` |
||||
type ArrowProps = { |
||||
direction: 'left' | 'right', |
||||
disabled?: boolean, |
||||
} |
||||
|
||||
export const Arrow = styled.span<ArrowProps>` |
||||
width: 1rem; |
||||
height: 1rem; |
||||
position: absolute; |
||||
border-left: 0.25rem solid #fff; |
||||
border-bottom: 0.25rem solid #fff; |
||||
top: 50%; |
||||
left: ${({ type }) => (type === 'arrowLeft' ? '10px' : 'calc(100% - 10px)')}; |
||||
width: 40px; |
||||
height: 40px; |
||||
background-position: center; |
||||
background-repeat: no-repeat; |
||||
background-image: url(${({ type }) => (type === 'arrowLeft' |
||||
? '/images/slideLeft.svg' |
||||
: '/images/slideRight.svg')}); |
||||
left: 50%; |
||||
border-radius: 3px; |
||||
|
||||
${({ direction }) => ( |
||||
direction === 'left' |
||||
? 'transform: translate(-50%, -50%) rotate(45deg);' |
||||
: 'transform: translate(-50%, -50%) rotate(225deg);' |
||||
)} |
||||
|
||||
${({ disabled }) => (disabled ? css` |
||||
border-left: 0.25rem solid gray; |
||||
border-bottom: 0.25rem solid gray; |
||||
` : '')}
|
||||
` |
||||
|
||||
export const ArrowButton = styled.button<ArrowProps>` |
||||
border: none; |
||||
outline: none; |
||||
padding: 0; |
||||
background-color: transparent; |
||||
cursor: pointer; |
||||
position: absolute; |
||||
width: 2.28rem; |
||||
height: 2.28rem; |
||||
z-index: 3; |
||||
top: 50%; |
||||
transform: translate(-50%, -50%); |
||||
z-index: 1; |
||||
left: ${({ direction }) => (direction === 'left' ? '1.6rem' : 'calc(100% - 1.6rem)')}; |
||||
|
||||
&:hover { |
||||
${({ direction, disabled }) => (!disabled ? css` |
||||
width: 3rem; |
||||
height: 3rem; |
||||
left: ${direction === 'left' ? '2rem' : 'calc(100% - 2rem)'}; |
||||
|
||||
${Arrow} { |
||||
width: 1.5rem; |
||||
height: 1.5rem; |
||||
} |
||||
` : '')}
|
||||
} |
||||
` |
||||
|
||||
@ -0,0 +1,111 @@ |
||||
import { |
||||
useCallback, |
||||
useEffect, |
||||
useMemo, |
||||
useState, |
||||
} from 'react' |
||||
|
||||
import { useRouteMatch } from 'react-router-dom' |
||||
|
||||
import { |
||||
MatchDto, |
||||
TimelineTournamentDto, |
||||
getTimelineMatches, |
||||
} from 'requests' |
||||
|
||||
import { Match, prepareMatches } from 'helpers' |
||||
|
||||
import { useAuthStore } from 'features/AuthStore' |
||||
import { useHeaderFiltersStore } from 'features/HeaderFilters' |
||||
|
||||
import { PAGES } from 'config' |
||||
|
||||
export type TimelineTournamentList = Array<Omit<TimelineTournamentDto, 'matches'> & { |
||||
matches: Array<Match>, |
||||
}> |
||||
|
||||
export const useTimeline = () => { |
||||
const isHomePage = useRouteMatch(PAGES.home)?.isExact |
||||
|
||||
const { user } = useAuthStore() |
||||
|
||||
const { |
||||
compareSport, |
||||
selectedMode, |
||||
selectedSport, |
||||
setSportIds, |
||||
} = useHeaderFiltersStore() |
||||
|
||||
const [isTimelineFetching, setIsTimelineFetching] = useState(true) |
||||
const [onlineUpcomingMatches, setOnlineUpcomingMatches] = useState<Array<Match>>([]) |
||||
const [tournamentList, setTournamentList] = useState<TimelineTournamentList>([]) |
||||
|
||||
const prepareMatchesDto = useCallback((matches: Array<MatchDto>) => prepareMatches( |
||||
matches, |
||||
user, |
||||
false, |
||||
), [user]) |
||||
|
||||
useEffect(() => { |
||||
(async () => { |
||||
setIsTimelineFetching(true) |
||||
try { |
||||
const timeline = await getTimelineMatches() |
||||
const convertedMatches = timeline.online_upcoming[0].matches |
||||
const preparedMatches = prepareMatchesDto(convertedMatches) |
||||
setOnlineUpcomingMatches(preparedMatches) |
||||
|
||||
setTournamentList([ |
||||
...timeline.favorite.map((item) => ({ |
||||
...item, |
||||
matches: prepareMatchesDto(item.matches), |
||||
})), |
||||
...timeline.promo.map((item) => ({ |
||||
...item, |
||||
matches: prepareMatchesDto(item.matches), |
||||
})), |
||||
...timeline.others.map((item) => ({ |
||||
...item, |
||||
matches: prepareMatchesDto(item.matches), |
||||
})), |
||||
]) |
||||
} catch (error) { /* empty */ } finally { |
||||
setIsTimelineFetching(false) |
||||
} |
||||
})() |
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []) |
||||
|
||||
const filteredTournamentsBySport = useMemo(() => { |
||||
if (isHomePage && selectedSport) { |
||||
return tournamentList.filter((t) => compareSport(t.sport_id, selectedSport)) |
||||
} |
||||
|
||||
return tournamentList |
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [tournamentList, selectedSport]) |
||||
|
||||
const filteredOnlineUpcomingBySport = useMemo(() => { |
||||
if (isHomePage && selectedSport) { |
||||
return onlineUpcomingMatches.filter((m) => compareSport(m.sportType, selectedSport)) |
||||
} |
||||
|
||||
return onlineUpcomingMatches |
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [onlineUpcomingMatches, selectedSport]) |
||||
|
||||
useEffect(() => { |
||||
if (!isHomePage) return |
||||
|
||||
const qwe = Array.from(new Set(tournamentList.map((t) => t.sport_id))) |
||||
setSportIds(qwe) |
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [tournamentList, selectedMode]) |
||||
|
||||
return { |
||||
isTimelineFetching, |
||||
onlineUpcomingMatches: filteredOnlineUpcomingBySport, |
||||
tournamentList: filteredTournamentsBySport, |
||||
} |
||||
} |
||||
|
||||
@ -0,0 +1,115 @@ |
||||
import { |
||||
useCallback, |
||||
useEffect, |
||||
useRef, |
||||
useState, |
||||
} from 'react' |
||||
|
||||
import { MatchesSlider } from 'features/MatchesSlider' |
||||
import { TimelineTournamentList, useTimeline } from 'features/MatchesTimeline/hooks' |
||||
import { InfiniteScroll } from 'features/InfiniteScroll' |
||||
import { T9n } from 'features/T9n' |
||||
import { TournamentSubtitle } from 'features/TournamentSubtitle' |
||||
|
||||
import { ProfileTypes } from 'config' |
||||
|
||||
import { |
||||
Content, |
||||
Tournament, |
||||
RowWrapper, |
||||
Wrapper, |
||||
TournamentLogo, |
||||
Loading, |
||||
TournamentSubtitleWrapper, |
||||
} from './styled' |
||||
|
||||
const TOURNAMENT_LIMIT = 6 |
||||
|
||||
export const MatchesTimeline = () => { |
||||
const { |
||||
isTimelineFetching, |
||||
onlineUpcomingMatches, |
||||
tournamentList, |
||||
} = useTimeline() |
||||
|
||||
const [tournaments, setTournaments] = useState<TimelineTournamentList>([]) |
||||
|
||||
const pageRef = useRef(0) |
||||
const isLastPageRef = useRef(false) |
||||
|
||||
const getTournaments = useCallback(() => tournamentList.slice( |
||||
pageRef.current * TOURNAMENT_LIMIT, |
||||
pageRef.current * TOURNAMENT_LIMIT + TOURNAMENT_LIMIT, |
||||
), [tournamentList]) |
||||
|
||||
const getMoreTournaments = () => { |
||||
if (isLastPageRef.current) return |
||||
const res = getTournaments() |
||||
|
||||
if (res.length) { |
||||
setTournaments((prev) => ([ |
||||
...prev, |
||||
...res, |
||||
])) |
||||
pageRef.current++ |
||||
} else { |
||||
isLastPageRef.current = true |
||||
} |
||||
} |
||||
|
||||
useEffect(() => { |
||||
if (tournamentList.length) { |
||||
pageRef.current = 0 |
||||
tournamentList.length && setTournaments(getTournaments()) |
||||
pageRef.current = 1 |
||||
} |
||||
}, [getTournaments, tournamentList]) |
||||
|
||||
if (isTimelineFetching) { |
||||
return <Loading><T9n t='loading' />...</Loading> |
||||
} |
||||
|
||||
return ( |
||||
<InfiniteScroll fullPageScroll onFetchMore={getMoreTournaments}> |
||||
<Wrapper> |
||||
{onlineUpcomingMatches.length > 0 && ( |
||||
<RowWrapper> |
||||
<Content> |
||||
<Tournament isOnlineUpcoming> |
||||
LIVE & UPCOMING |
||||
</Tournament> |
||||
<MatchesSlider matches={onlineUpcomingMatches} /> |
||||
</Content> |
||||
</RowWrapper> |
||||
)} |
||||
{tournaments.map(({ |
||||
matches, |
||||
sport_id, |
||||
tournament, |
||||
tournament_id, |
||||
}) => ( |
||||
<RowWrapper key={tournament_id}> |
||||
<TournamentSubtitleWrapper> |
||||
<TournamentSubtitle |
||||
sportInfo={matches[0].sportInfo} |
||||
countryId={matches[0].countryId} |
||||
sportType={sport_id} |
||||
tournament={tournament} |
||||
/> |
||||
</TournamentSubtitleWrapper> |
||||
<Content> |
||||
<Tournament gradientColor={tournament.color}> |
||||
<TournamentLogo |
||||
id={tournament_id} |
||||
profileType={ProfileTypes.TOURNAMENTS} |
||||
sportType={sport_id} |
||||
/> |
||||
</Tournament> |
||||
<MatchesSlider matches={matches} /> |
||||
</Content> |
||||
</RowWrapper> |
||||
))} |
||||
</Wrapper> |
||||
</InfiniteScroll> |
||||
) |
||||
} |
||||
@ -0,0 +1,90 @@ |
||||
import { ProfileLogo } from 'features/ProfileLogo' |
||||
import { StyledLink } from 'features/TournamentSubtitle/styled' |
||||
import styled, { css } from 'styled-components/macro' |
||||
|
||||
export const Wrapper = styled.div` |
||||
& > * { |
||||
margin-bottom: 20px; |
||||
} |
||||
` |
||||
|
||||
export const RowWrapper = styled.div`` |
||||
|
||||
export const Content = styled.div` |
||||
display: flex; |
||||
gap: 10px; |
||||
` |
||||
|
||||
export const Tournament = styled.div<{ |
||||
gradientColor?: string, |
||||
isOnlineUpcoming?: boolean, |
||||
}>` |
||||
${({ gradientColor }) => (gradientColor |
||||
? css`background: linear-gradient(187deg, ${gradientColor} -4.49%, #000000 68.29%), #000000;` |
||||
: css`background-color: ${({ theme }) => theme.colors.matchCardBackground};`)} |
||||
|
||||
// в будущем от этого нужно будет избавиться
|
||||
${({ isOnlineUpcoming }) => isOnlineUpcoming && css` |
||||
background: linear-gradient(270deg, #C00 0%, #6A2131 100%); |
||||
`}
|
||||
|
||||
position: relative; |
||||
height: 12.9rem; |
||||
width: 12.9rem; |
||||
flex-shrink: 0; |
||||
margin: 10px 0; |
||||
display: flex; |
||||
align-items: center; |
||||
justify-content: center; |
||||
border-radius: 3px; |
||||
font-size: 1.9rem; |
||||
font-weight: 700; |
||||
color: #FFFFFF; |
||||
text-align: start; |
||||
padding: 10px; |
||||
|
||||
@media screen and (min-width: 1920px) { |
||||
height: 13.8rem; |
||||
width: 13.8rem; |
||||
} |
||||
|
||||
@media screen and (max-width: 1920px) { |
||||
height: 13.4rem; |
||||
width: 13.4rem; |
||||
} |
||||
|
||||
@media screen and (max-width: 1440px) { |
||||
height: 12.8rem; |
||||
width: 12.8rem; |
||||
} |
||||
|
||||
@media screen and (max-width: 1280px) { |
||||
height: 12.6rem; |
||||
width: 12.6rem; |
||||
} |
||||
` |
||||
|
||||
export const TournamentLogo = styled(ProfileLogo)` |
||||
position: absolute; |
||||
max-height: 100%; |
||||
padding: 20px; |
||||
` |
||||
|
||||
export const Loading = styled.div` |
||||
height: 30px; |
||||
margin-top: 20px; |
||||
font-size: 24px; |
||||
color: #fff; |
||||
text-align: center; |
||||
` |
||||
|
||||
export const TournamentSubtitleWrapper = styled.div` |
||||
margin-bottom: 10px; |
||||
|
||||
${StyledLink} { |
||||
font-weight: 700; |
||||
color: #FFFFFF; |
||||
font-size: 1rem; |
||||
text-transform: uppercase; |
||||
} |
||||
` |
||||
@ -0,0 +1,35 @@ |
||||
import { API_ROOT } from 'config' |
||||
|
||||
import { callApi } from 'helpers' |
||||
import type { MatchesDto, TournamentType } from 'requests' |
||||
|
||||
export type TimelineTournamentDto = { |
||||
matches: MatchesDto, |
||||
sport_id: number, |
||||
tournament: TournamentType, |
||||
tournament_id: number, |
||||
} |
||||
|
||||
export type MatchesTimeline = { |
||||
favorite: Array<TimelineTournamentDto>, |
||||
online_upcoming: Array<{matches: MatchesDto}>, |
||||
others: Array<TimelineTournamentDto>, |
||||
promo: Array<TimelineTournamentDto>, |
||||
} |
||||
|
||||
export const getTimelineMatches = (sportId?: number): Promise<MatchesTimeline> => { |
||||
const url = new URL(`${API_ROOT}/v1/matches/timeline`) |
||||
|
||||
if (sportId) { |
||||
url.searchParams.append('sport_id', `${sportId}`) |
||||
} |
||||
|
||||
const config = { |
||||
method: 'GET', |
||||
} |
||||
|
||||
return callApi({ |
||||
config, |
||||
url: url.href, |
||||
}) |
||||
} |
||||
Loading…
Reference in new issue