diff --git a/public/images/closeIcon.svg b/public/images/closeIcon.svg
new file mode 100644
index 00000000..c14ef319
--- /dev/null
+++ b/public/images/closeIcon.svg
@@ -0,0 +1,3 @@
+
diff --git a/public/images/preview.png b/public/images/preview.png
new file mode 100644
index 00000000..c835be5b
Binary files /dev/null and b/public/images/preview.png differ
diff --git a/public/images/slideLeft.svg b/public/images/slideLeft.svg
new file mode 100644
index 00000000..b7ba2449
--- /dev/null
+++ b/public/images/slideLeft.svg
@@ -0,0 +1,5 @@
+
diff --git a/public/images/slideRight.svg b/public/images/slideRight.svg
new file mode 100644
index 00000000..13140102
--- /dev/null
+++ b/public/images/slideRight.svg
@@ -0,0 +1,5 @@
+
diff --git a/src/config/lexics/authenticated.tsx b/src/config/lexics/authenticated.tsx
index c9d0a0fc..db3fd103 100644
--- a/src/config/lexics/authenticated.tsx
+++ b/src/config/lexics/authenticated.tsx
@@ -3,17 +3,31 @@ import { proceduresLexics } from './procedures'
export const authenticatedLexics = {
basketball: 6960,
football: 6958,
+ full_game: 13028,
+ game_finished: 13026,
+ game_time: 13029,
+ goals: 13030,
hide_score: 12982,
+ highlights: 13033,
hockey: 6959,
+ interview: 13031,
+ kickoff_in: 13027,
+ live: 13024,
logout: 4306,
match_status_finished: 12985,
match_status_live: 12984,
match_status_soon: 12986,
+ match_video: 13025,
player: 630,
+ players_video: 13032,
sport: 12993,
team: 658,
tournament: 1009,
user_account: 12928,
+ video_from_my_subscriptions: 13023,
+ watch_from_beginning: 13021,
+ watch_from_last_pause: 13022,
+ watch_now: 13020,
...proceduresLexics,
}
diff --git a/src/config/sportTypes.tsx b/src/config/sportTypes.tsx
index 1eaedd7c..995131c4 100644
--- a/src/config/sportTypes.tsx
+++ b/src/config/sportTypes.tsx
@@ -8,4 +8,4 @@ export const SPORT_NAMES = {
[SportTypes.BASKETBALL]: 'basketball',
[SportTypes.FOOTBALL]: 'football',
[SportTypes.HOCKEY]: 'hockey',
-}
+} as const
diff --git a/src/features/Common/CloseButton/index.tsx b/src/features/Common/CloseButton/index.tsx
new file mode 100644
index 00000000..d4cb9d3e
--- /dev/null
+++ b/src/features/Common/CloseButton/index.tsx
@@ -0,0 +1,18 @@
+import styled from 'styled-components/macro'
+
+export const CloseButton = styled.button.attrs({
+ 'aria-label': 'Close',
+})`
+ position: absolute;
+ top: 0;
+ right: 0;
+ width: 10px;
+ height: 10px;
+ cursor: pointer;
+ border-style: none;
+ outline: none;
+ background: none;
+ background-image: url(/images/closeIcon.svg);
+ background-repeat: no-repeat;
+ background-size: contain;
+`
diff --git a/src/features/Common/SportName/index.tsx b/src/features/Common/SportName/index.tsx
new file mode 100644
index 00000000..2728ecaf
--- /dev/null
+++ b/src/features/Common/SportName/index.tsx
@@ -0,0 +1,11 @@
+import styled from 'styled-components/macro'
+
+import { T9n } from 'features/T9n'
+
+export const SportName = styled(T9n)<{ color: string }>`
+ margin-right: 10px;
+ font-size: 11px;
+ color: ${({ color }) => color};
+ letter-spacing: 0.02em;
+ text-transform: uppercase;
+`
diff --git a/src/features/Common/index.tsx b/src/features/Common/index.tsx
index 9ed1199f..78bf42cc 100644
--- a/src/features/Common/index.tsx
+++ b/src/features/Common/index.tsx
@@ -3,5 +3,7 @@ export * from './Button'
export * from './Radio'
export * from './Checkbox'
export * from './Arrows'
+export * from './CloseButton'
+export * from './SportName'
export * from './сustomScrollbar'
export * from './customStyles'
diff --git a/src/features/HeaderFilters/components/SportTypeFilter/styled.tsx b/src/features/HeaderFilters/components/SportTypeFilter/styled.tsx
index c76420f1..102e59ac 100644
--- a/src/features/HeaderFilters/components/SportTypeFilter/styled.tsx
+++ b/src/features/HeaderFilters/components/SportTypeFilter/styled.tsx
@@ -24,6 +24,7 @@ export const SportList = styled.ul`
background: #666;
border-radius: 2px;
box-shadow: 0px 1px 1px rgba(0, 0, 0, 0.3);
+ z-index: 1;
`
export const CustomOption = styled(T9n)<{ image: string }>`
diff --git a/src/features/HeaderFilters/components/TournamentFilter/styled.tsx b/src/features/HeaderFilters/components/TournamentFilter/styled.tsx
index 4360872d..9326e195 100644
--- a/src/features/HeaderFilters/components/TournamentFilter/styled.tsx
+++ b/src/features/HeaderFilters/components/TournamentFilter/styled.tsx
@@ -9,6 +9,7 @@ export const ListWrapper = styled.div`
width: 448px;
height: 456px;
overflow-y: scroll;
+ z-index: 2;
${сustomScrollbar}
`
diff --git a/src/features/HeaderFilters/components/TournamentList/index.tsx b/src/features/HeaderFilters/components/TournamentList/index.tsx
index 79b3a8b4..ee220270 100644
--- a/src/features/HeaderFilters/components/TournamentList/index.tsx
+++ b/src/features/HeaderFilters/components/TournamentList/index.tsx
@@ -8,10 +8,10 @@ import {
LogoWrapper,
ItemInfo,
Name,
- SportName,
TeamOrCountry,
Wrapper,
} from 'features/ItemsList/styled'
+import { SportName } from 'features/Common'
import { ListItem } from './styled'
diff --git a/src/features/HeaderFilters/store/hooks/index.tsx b/src/features/HeaderFilters/store/hooks/index.tsx
index 0206b386..89d157bb 100644
--- a/src/features/HeaderFilters/store/hooks/index.tsx
+++ b/src/features/HeaderFilters/store/hooks/index.tsx
@@ -5,49 +5,124 @@ import {
useCallback,
} from 'react'
-import debounce from 'lodash/debounce'
+import map from 'lodash/map'
+import flatten from 'lodash/flatten'
+import pipe from 'lodash/fp/pipe'
+import fpMap from 'lodash/fp/map'
+import fpOrderBy from 'lodash/fp/orderBy'
import format from 'date-fns/format'
import startOfDay from 'date-fns/startOfDay'
-import { SportTypes } from 'config'
+import type { Matches, Content } from 'requests'
+import { SportTypes, SPORT_NAMES } from 'config'
import { getMatches } from 'requests'
+import { useLexicsStore } from 'features/LexicsStore'
+import { getProfileLogo } from 'helpers'
+
+type Name = 'name_rus' | 'name_eng'
+
+export type Match = {
+ date: string,
+ id: number,
+ preview: string,
+ sportName: string,
+ sportType: number,
+ streamStatus: number,
+ team1Logo: string,
+ team1Name: string,
+ team1Score: number,
+ team2Logo: string,
+ team2Name: string,
+ team2Score: number,
+ tournamentName: string,
+}
export enum MatchStatuses {
- Live = 1,
- Finished = 2,
- Soon = 3,
+ Live = 2,
+ Finished = 3,
+ Soon = 1,
}
const dateFormat = 'dd/MM/yyyy HH:mm:ss'
export const useFilters = () => {
+ const {
+ suffix,
+ } = useLexicsStore()
const [selectedDate, setSelectedDate] = useState(new Date())
const [selectedMatchStatus, setSelectedMatchStatus] = useState(MatchStatuses.Live)
const [selectedSportTypeId, setSelectedSportTypeId] = useState(SportTypes.FOOTBALL)
const [selectedTournamentId, setSelectedTournamentId] = useState(null)
- const fetchMatches = useCallback(debounce(getMatches, 300), [])
+ const [matches, setMatches] = useState({
+ broadcast: [],
+ features: [],
+ highlights: [],
+ })
// временно здесь запрашиваются матчи при изменении фильтров,
// можно эту логику вырезать и вставить в компонент матчей
useEffect(() => {
- if (!selectedTournamentId) return
-
const formattedDate = format(startOfDay(selectedDate), dateFormat)
- fetchMatches({
+
+ getMatches({
date: formattedDate,
matchStatus: selectedMatchStatus,
sportType: selectedSportTypeId,
tournamentId: selectedTournamentId,
- })
+ }).then(setMatches)
}, [
selectedDate,
selectedMatchStatus,
selectedSportTypeId,
selectedTournamentId,
- fetchMatches,
])
+ const prepareMatches = useCallback((content: Array) => pipe(
+ fpMap>(({
+ matches: matchesList,
+ sport,
+ ...rest
+ }) => map(matchesList, ({
+ date,
+ id,
+ stream_status,
+ team1,
+ team2,
+ }) => ({
+ date,
+ id,
+ preview: '/images/preview.png',
+ sportName: SPORT_NAMES[sport],
+ sportType: sport,
+ streamStatus: stream_status,
+ team1Logo: getProfileLogo({
+ id: team1.id,
+ profileType: 2,
+ sportType: sport,
+ }),
+ team1Name: team1[`name_${suffix}` as Name],
+ team1Score: team1.score,
+ team2Logo: getProfileLogo({
+ id: team2.id,
+ profileType: 2,
+ sportType: sport,
+ }),
+ team2Name: team2[`name_${suffix}` as Name],
+ team2Score: team2.score,
+ tournamentName: rest[`name_${suffix}` as Name],
+ }))),
+ flatten,
+ fpOrderBy((match: Match) => Number(new Date(match.date)), 'desc'),
+ )(content) as Array, [suffix])
+
+ const preparedMatches = {
+ broadcast: prepareMatches(matches.broadcast),
+ features: prepareMatches(matches.features),
+ highlights: prepareMatches(matches.highlights),
+ }
+
const store = useMemo(() => ({
+ matches: preparedMatches,
selectedDate,
selectedMatchStatus,
selectedSportTypeId,
@@ -62,6 +137,7 @@ export const useFilters = () => {
selectedSportTypeId,
selectedTournamentId,
setSelectedTournamentId,
+ preparedMatches,
])
return store
diff --git a/src/features/HomePage/components/Matches/hooks.tsx b/src/features/HomePage/components/Matches/hooks.tsx
new file mode 100644
index 00000000..7d3fd7c8
--- /dev/null
+++ b/src/features/HomePage/components/Matches/hooks.tsx
@@ -0,0 +1,45 @@
+import size from 'lodash/size'
+import slice from 'lodash/slice'
+import chunk from 'lodash/chunk'
+
+import type { Match } from 'features/HeaderFilters'
+import { useHeaderFiltersStore } from 'features/HeaderFilters'
+
+const MATCHES_PER_ROW = 6
+
+const ROWS_COUNT = 3
+
+export const useMatches = () => {
+ const {
+ matches: {
+ broadcast: matchesList,
+ },
+ } = useHeaderFiltersStore()
+ const matchesCount = size(matchesList)
+
+ let groupedMatches: Array> = []
+
+ /**
+ * Распределяем матчи в ROWS_COUNT рядов
+ * Если общее количество матчей не больше количества
+ * ячеек в сетке размера ROWS_COUNT x MATCHES_PER_ROW,
+ * заполняем по максимуму каждый ряд с MATCHES_PER_ROW
+ * матчей в каждом, иначе распределяем поровну
+ */
+ if (matchesCount <= MATCHES_PER_ROW * ROWS_COUNT) {
+ for (let i = 0; i < ROWS_COUNT; i++) {
+ const matches = slice(
+ matchesList,
+ i * MATCHES_PER_ROW,
+ i * MATCHES_PER_ROW + MATCHES_PER_ROW,
+ )
+ groupedMatches.push(matches)
+ }
+ } else {
+ groupedMatches = chunk(matchesList, Math.ceil(matchesCount / ROWS_COUNT))
+ }
+
+ return {
+ groupedMatches,
+ }
+}
diff --git a/src/features/HomePage/components/Matches/index.tsx b/src/features/HomePage/components/Matches/index.tsx
new file mode 100644
index 00000000..5309f433
--- /dev/null
+++ b/src/features/HomePage/components/Matches/index.tsx
@@ -0,0 +1,22 @@
+import React, { Fragment } from 'react'
+
+import map from 'lodash/map'
+
+import { MatchesSlider } from 'features/MatchesSlider'
+
+import { useMatches } from './hooks'
+
+export const Matches = () => {
+ const { groupedMatches } = useMatches()
+
+ return (
+
+ {map(groupedMatches, (matches, i) => (
+
+ ))}
+
+ )
+}
diff --git a/src/features/HomePage/index.tsx b/src/features/HomePage/index.tsx
index c04e4986..93a16533 100644
--- a/src/features/HomePage/index.tsx
+++ b/src/features/HomePage/index.tsx
@@ -11,9 +11,13 @@ import {
HeaderFiltersStore,
} from 'features/HeaderFilters'
import { UserFavorites } from 'features/UserFavorites'
+import { T9n } from 'features/T9n'
+import { Matches } from './components/Matches'
import {
FilterWrapper,
+ Title,
+ Content,
} from './styled'
export const HomePage = () => (
@@ -38,6 +42,10 @@ export const HomePage = () => (
+
+
+
+
)
diff --git a/src/features/HomePage/styled.tsx b/src/features/HomePage/styled.tsx
index 8996f49b..b086e528 100644
--- a/src/features/HomePage/styled.tsx
+++ b/src/features/HomePage/styled.tsx
@@ -6,3 +6,17 @@ export const FilterWrapper = styled.div`
margin-right: 16px;
display: flex;
`
+
+export const Content = styled.main`
+ margin-top: 75px;
+ padding: 0 16px;
+`
+
+export const Title = styled.h1`
+ margin-bottom: 17px;
+ font-weight: bold;
+ font-size: 36px;
+ line-height: 24px;
+ letter-spacing: -0.02em;
+ color: #fff;
+`
diff --git a/src/features/ItemsList/index.tsx b/src/features/ItemsList/index.tsx
index a79dc75a..237fbdc6 100644
--- a/src/features/ItemsList/index.tsx
+++ b/src/features/ItemsList/index.tsx
@@ -2,6 +2,8 @@ import React from 'react'
import map from 'lodash/map'
+import { SportName } from 'features/Common'
+
import { useItemsList } from './hooks'
import {
Logo,
@@ -9,7 +11,6 @@ import {
Item,
ItemInfo,
Name,
- SportName,
StyledLink,
TeamOrCountry,
Wrapper,
diff --git a/src/features/ItemsList/styled.tsx b/src/features/ItemsList/styled.tsx
index 6fded63e..8c2dea79 100644
--- a/src/features/ItemsList/styled.tsx
+++ b/src/features/ItemsList/styled.tsx
@@ -2,8 +2,6 @@ import { Link } from 'react-router-dom'
import styled from 'styled-components/macro'
-import { T9n } from 'features/T9n'
-
export const Wrapper = styled.ul`
margin: 0;
padding: 0;
@@ -40,14 +38,6 @@ export const Name = styled.div`
color: #ccc;
`
-export const SportName = styled(T9n)<{ color: string }>`
- margin-right: 10px;
- font-size: 11px;
- color: ${({ color }) => color};
- letter-spacing: 0.02em;
- text-transform: uppercase;
-`
-
export const TeamOrCountry = styled.span`
font-size: 11px;
color: #ccc;
diff --git a/src/features/MainWrapper/index.tsx b/src/features/MainWrapper/index.tsx
index 10d22364..40b8e074 100644
--- a/src/features/MainWrapper/index.tsx
+++ b/src/features/MainWrapper/index.tsx
@@ -2,5 +2,5 @@ import styled from 'styled-components/macro'
export const MainWrapper = styled.div`
width: 100%;
- display: flex;
+ padding-left: 80px;
`
diff --git a/src/features/MatchesSlider/components/CardFinished/index.tsx b/src/features/MatchesSlider/components/CardFinished/index.tsx
new file mode 100644
index 00000000..6f5790cf
--- /dev/null
+++ b/src/features/MatchesSlider/components/CardFinished/index.tsx
@@ -0,0 +1,85 @@
+import React from 'react'
+
+import styled from 'styled-components/macro'
+
+import type { Match } from 'features/HeaderFilters'
+import { getSportColor } from 'helpers'
+import { useCard } from 'features/MatchesSlider/hooks'
+import { SportName } from 'features/Common'
+import { T9n } from 'features/T9n'
+
+import { CardFinishedHover } from '../CardFinishedHover'
+import {
+ CardWrapper,
+ Info,
+ MatchStatus as CommonMatchStatus,
+ Preview,
+ PreviewWrapper,
+ Score,
+ Team,
+ TeamName,
+ Teams,
+ TournamentName,
+} from '../styled'
+
+const MatchStatus = styled(CommonMatchStatus)`
+ color: #313131;
+ background-color: #EACB6F;
+`
+
+type CardFinishedProps = {
+ match: Match,
+}
+
+export const CardFinished = ({
+ match: {
+ date,
+ preview,
+ sportName,
+ sportType,
+ team1Name,
+ team1Score,
+ team2Name,
+ team2Score,
+ tournamentName,
+ },
+}: CardFinishedProps) => {
+ const {
+ close,
+ isOpen,
+ onKeyPress,
+ open,
+ } = useCard()
+
+ if (isOpen) return
+
+ return (
+
+
+
+
+
+
+
+ {tournamentName}
+
+
+ {team1Name}
+ {team1Score}
+
+
+ {team2Name}
+ {team2Score}
+
+
+
+
+ )
+}
diff --git a/src/features/MatchesSlider/components/CardFinishedHover/index.tsx b/src/features/MatchesSlider/components/CardFinishedHover/index.tsx
new file mode 100644
index 00000000..c0897401
--- /dev/null
+++ b/src/features/MatchesSlider/components/CardFinishedHover/index.tsx
@@ -0,0 +1,41 @@
+import React from 'react'
+
+import { CloseButton } from 'features/Common'
+
+import {
+ CardHoverInner,
+ CardHoverTitle,
+ CardHoverWrapper,
+ MoreVideo,
+ Row,
+ Rows,
+} from '../styled'
+
+type CardFinishedHoverProps = {
+ onClose: () => void,
+}
+
+export const CardFinishedHover = ({ onClose }: CardFinishedHoverProps) => (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+)
diff --git a/src/features/MatchesSlider/components/CardLive/index.tsx b/src/features/MatchesSlider/components/CardLive/index.tsx
new file mode 100644
index 00000000..c3c0daaa
--- /dev/null
+++ b/src/features/MatchesSlider/components/CardLive/index.tsx
@@ -0,0 +1,109 @@
+import React, {
+ useState,
+ useEffect,
+ useCallback,
+} from 'react'
+
+import styled from 'styled-components/macro'
+import differenceInMilliseconds from 'date-fns/differenceInMilliseconds'
+
+import type { Match } from 'features/HeaderFilters'
+import { getSportColor, msToMinutesAndSeconds } from 'helpers'
+import { useCard } from 'features/MatchesSlider/hooks'
+import { SportName } from 'features/Common'
+import { T9n } from 'features/T9n'
+
+import {
+ CardWrapper,
+ Info,
+ MatchStatus as CommonMatchStatus,
+ Preview,
+ PreviewWrapper,
+ Score,
+ Team,
+ TeamName,
+ Teams,
+ TournamentName,
+} from '../styled'
+import { CardLiveHover } from '../CardLiveHover'
+
+const UPDATE_INTERVAL = 1000
+
+const MatchStatus = styled(CommonMatchStatus)`
+ min-width: 100px;
+ color: #fff;
+ background-color: #cc0000;
+`
+
+type CardLiveProps = {
+ match: Match,
+}
+
+export const CardLive = ({
+ match: {
+ date,
+ id,
+ preview,
+ sportName,
+ sportType,
+ team1Name,
+ team1Score,
+ team2Name,
+ team2Score,
+ tournamentName,
+ },
+}: CardLiveProps) => {
+ const {
+ close,
+ isOpen,
+ onKeyPress,
+ open,
+ } = useCard()
+
+ const getMs = useCallback(() => differenceInMilliseconds(
+ new Date(),
+ new Date(date),
+ ), [date])
+
+ const [matchDuration, setMatchDuration] = useState(msToMinutesAndSeconds(getMs()))
+
+ useEffect(() => {
+ const timer = setInterval(() => {
+ setMatchDuration(msToMinutesAndSeconds(getMs()))
+ }, UPDATE_INTERVAL)
+
+ return () => clearInterval(timer)
+ }, [getMs])
+
+ if (isOpen) return
+
+ return (
+
+ {matchDuration}
+
+
+
+
+
+ {tournamentName}
+
+
+ {team1Name}
+ {team1Score}
+
+
+ {team2Name}
+ {team2Score}
+
+
+
+
+ )
+}
diff --git a/src/features/MatchesSlider/components/CardLiveHover/index.tsx b/src/features/MatchesSlider/components/CardLiveHover/index.tsx
new file mode 100644
index 00000000..03ecceb2
--- /dev/null
+++ b/src/features/MatchesSlider/components/CardLiveHover/index.tsx
@@ -0,0 +1,35 @@
+import React from 'react'
+
+import { CloseButton } from 'features/Common'
+
+import {
+ CardHoverInner,
+ CardHoverTitle,
+ CardHoverWrapper,
+ MoreVideo,
+ Row,
+ Rows,
+} from '../styled'
+
+type CardLiveHoverProps = {
+ onClose: () => void,
+}
+
+export const CardLiveHover = ({ onClose }: CardLiveHoverProps) => (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+)
diff --git a/src/features/MatchesSlider/components/CardSoon/index.tsx b/src/features/MatchesSlider/components/CardSoon/index.tsx
new file mode 100644
index 00000000..fa56be03
--- /dev/null
+++ b/src/features/MatchesSlider/components/CardSoon/index.tsx
@@ -0,0 +1,102 @@
+import type { BaseSyntheticEvent } from 'react'
+import React, { useCallback } from 'react'
+
+import styled from 'styled-components/macro'
+import format from 'date-fns/format'
+
+import type { Match } from 'features/HeaderFilters'
+import { getSportColor, handleImageError } from 'helpers'
+import { SportName } from 'features/Common'
+import { T9n } from 'features/T9n'
+
+import {
+ MatchStatus as CommonMatchStatus,
+ CardWrapper as CommonCardWrapper,
+ Info,
+ PreviewWrapper,
+ Team,
+ TeamName as CommonTeamName,
+ Teams,
+ TournamentName,
+} from '../styled'
+
+const MatchStatus = styled(CommonMatchStatus)`
+ color: rgba(255, 255, 255, 0.3);
+ border: 1px solid currentColor;
+`
+
+const CardWrapper = styled(CommonCardWrapper)`
+ cursor: default;
+`
+
+const TeamLogos = styled.div`
+ display: flex;
+ padding-left: 24px;
+`
+
+const TeamLogo = styled.img`
+ width: 70px;
+`
+
+const TeamName = styled(CommonTeamName)`
+ max-width: none;
+`
+
+type CardSoonProps = {
+ match: Match,
+}
+
+export const CardSoon = ({
+ match: {
+ date,
+ sportName,
+ sportType,
+ team1Logo,
+ team1Name,
+ team2Logo,
+ team2Name,
+ tournamentName,
+ },
+}: CardSoonProps) => {
+ const startTime = format(new Date(date), 'HH:mm')
+
+ const onError = useCallback((e: BaseSyntheticEvent) => handleImageError({
+ e,
+ sport: sportType,
+ type: 2,
+ }), [sportType])
+
+ return (
+
+ {startTime}
+
+
+
+
+
+
+
+
+ {tournamentName}
+
+
+ {team1Name}
+
+
+ {team2Name}
+
+
+
+
+ )
+}
diff --git a/src/features/MatchesSlider/components/MatchCard/index.tsx b/src/features/MatchesSlider/components/MatchCard/index.tsx
new file mode 100644
index 00000000..99b1e567
--- /dev/null
+++ b/src/features/MatchesSlider/components/MatchCard/index.tsx
@@ -0,0 +1,21 @@
+import React from 'react'
+
+import type { Match } from 'features/HeaderFilters'
+import { MatchStatuses } from 'features/HeaderFilters'
+
+import { CardLive } from '../CardLive'
+import { CardFinished } from '../CardFinished'
+import { CardSoon } from '../CardSoon'
+
+type MatchCardProps = {
+ match: Match,
+}
+
+export const MatchCard = ({ match }: MatchCardProps) => {
+ switch (match.streamStatus) {
+ case MatchStatuses.Soon: return
+ case MatchStatuses.Live: return
+ case MatchStatuses.Finished: return
+ default: return null
+ }
+}
diff --git a/src/features/MatchesSlider/components/styled.tsx b/src/features/MatchesSlider/components/styled.tsx
new file mode 100644
index 00000000..7d418f0a
--- /dev/null
+++ b/src/features/MatchesSlider/components/styled.tsx
@@ -0,0 +1,122 @@
+import styled from 'styled-components/macro'
+
+import { T9n } from 'features/T9n'
+
+import { MATCH_CARD_WIDTH, MATCH_CARD_GAP } from '../config'
+
+export const CardWrapper = styled.li.attrs({
+ tabIndex: 0,
+})`
+ position: relative;
+ flex: 0 0 auto;
+ width: ${MATCH_CARD_WIDTH}px;
+ margin-right: ${MATCH_CARD_GAP}px;
+ padding-bottom: 38px;
+ border-radius: 2px;
+ background-color: #3F3F3F;
+ box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.4);
+ cursor: pointer;
+`
+
+export const PreviewWrapper = styled.div`
+ display: flex;
+ flex-direction: column-reverse;
+ height: 160px;
+`
+
+export const Preview = styled.img`
+ width: 100%;
+ height: 100%;
+`
+
+export const MatchStatus = styled.div`
+ position: absolute;
+ top: 24px;
+ left: 24px;
+ border-radius: 2px;
+ padding: 7px 10px;
+ font-size: 12px;
+ font-weight: bold;
+ text-transform: uppercase;
+ letter-spacing: 0.1em;
+`
+
+export const Info = styled.div`
+ padding: 27px 24px 0;
+ color: #fff;
+`
+
+export const TournamentName = styled.span`
+ color: rgba(255, 255, 255, 0.5);
+ font-size: 10px;
+`
+
+export const Teams = styled.div`
+ margin-top: 21px;
+`
+
+export const Team = styled.div`
+ display: flex;
+ justify-content: space-between;
+ margin-bottom: 5px;
+ font-size: 17px;
+ font-weight: bold;
+ line-height: 21px;
+ color: #fff;
+`
+
+export const TeamName = styled.span`
+ max-width: 90%;
+ word-break: break-word;
+`
+
+export const Score = styled.span``
+
+export const Rows = styled.div`
+ margin-top: 20px;
+`
+
+export const Row = styled.div`
+ white-space: nowrap;
+`
+
+export const MoreVideo = styled(T9n)`
+ display: inline-block;
+ margin: 0 8px 8px 0;
+ padding: 8px;
+ border-radius: 2px;
+ font-weight: 500;
+ font-size: 11px;
+ text-align: center;
+ color: rgba(255, 255, 255, 0.5);
+ background: linear-gradient(
+ 180deg,
+ rgba(255, 255, 255, 0.1) 0%,
+ rgba(255, 255, 255, 0) 100%
+ ),
+ #5C5C5C;
+ box-shadow: 0px 1px 0px rgba(0, 0, 0, 0.1);
+ cursor: pointer;
+
+ :hover {
+ background: rgba(153, 153, 153, 0.9);
+ color: #fff;
+ }
+`
+
+export const CardHoverWrapper = styled(CardWrapper)`
+ padding: 16px 24px;
+ cursor: default;
+`
+
+export const CardHoverInner = styled.div`
+ position: relative;
+ overflow: hidden;
+`
+
+export const CardHoverTitle = styled(T9n)`
+ font-size: 10px;
+ letter-spacing: 0.03em;
+ text-transform: uppercase;
+ color: rgba(255, 255, 255, 0.5);
+`
diff --git a/src/features/MatchesSlider/config/index.tsx b/src/features/MatchesSlider/config/index.tsx
new file mode 100644
index 00000000..a42755d3
--- /dev/null
+++ b/src/features/MatchesSlider/config/index.tsx
@@ -0,0 +1,3 @@
+export const MATCH_CARD_WIDTH = 288
+
+export const MATCH_CARD_GAP = 16
diff --git a/src/features/MatchesSlider/hooks.tsx b/src/features/MatchesSlider/hooks.tsx
new file mode 100644
index 00000000..bda5298e
--- /dev/null
+++ b/src/features/MatchesSlider/hooks.tsx
@@ -0,0 +1,79 @@
+import type { SyntheticEvent, KeyboardEvent } from 'react'
+import {
+ useEffect,
+ useRef,
+ useState,
+ useCallback,
+} from 'react'
+
+import type { Match } from 'features/HeaderFilters'
+import { useToggle } from 'hooks'
+
+import { MATCH_CARD_WIDTH, MATCH_CARD_GAP } from './config'
+
+export const useCard = () => {
+ const {
+ close,
+ isOpen,
+ open,
+ } = useToggle()
+
+ const onKeyPress = useCallback((e: KeyboardEvent) => {
+ if (e.key === 'Enter') {
+ open()
+ }
+ }, [open])
+
+ return {
+ close,
+ isOpen,
+ onKeyPress,
+ open,
+ }
+}
+
+export const useMatchesSlider = (matches: Array) => {
+ const slidesRef = useRef(null)
+
+ const [showLeftArrow, setShowLeftArrow] = useState(false)
+ const [showRightArrow, setShowRigthArrow] = useState(false)
+
+ useEffect(() => {
+ const {
+ clientWidth = 0,
+ scrollLeft = 0,
+ scrollWidth = 0,
+ } = slidesRef.current || {}
+ const scrollRight = scrollWidth - (scrollLeft + clientWidth)
+
+ setShowRigthArrow(scrollRight > 1)
+ }, [matches, slidesRef])
+
+ const onScroll = useCallback((e: SyntheticEvent) => {
+ const {
+ clientWidth: targetClientWidth,
+ scrollLeft: targetScrollLeft,
+ scrollWidth: targetScrollWidth,
+ } = e.currentTarget
+
+ setShowLeftArrow(targetScrollLeft > 1)
+ setShowRigthArrow((targetScrollWidth - (targetScrollLeft + targetClientWidth)) > 1)
+ }, [])
+
+ const slideLeft = useCallback(() => {
+ slidesRef.current!.scrollBy(-(MATCH_CARD_WIDTH + MATCH_CARD_GAP), 0)
+ }, [])
+
+ const slideRight = useCallback(() => {
+ slidesRef.current!.scrollBy(MATCH_CARD_WIDTH + MATCH_CARD_GAP, 0)
+ }, [])
+
+ return {
+ onScroll,
+ showLeftArrow,
+ showRightArrow,
+ slideLeft,
+ slideRight,
+ slidesRef,
+ }
+}
diff --git a/src/features/MatchesSlider/index.tsx b/src/features/MatchesSlider/index.tsx
new file mode 100644
index 00000000..9ac322e0
--- /dev/null
+++ b/src/features/MatchesSlider/index.tsx
@@ -0,0 +1,53 @@
+import React from 'react'
+
+import map from 'lodash/map'
+import isEmpty from 'lodash/isEmpty'
+
+import type { Match } from 'features/HeaderFilters'
+
+import { useMatchesSlider } from './hooks'
+import { MatchCard } from './components/MatchCard'
+import {
+ Wrapper,
+ Arrow,
+ Slides,
+} from './styled'
+
+type MatchesSliderProps = {
+ matches: Array,
+}
+
+export const MatchesSlider = ({ matches }: MatchesSliderProps) => {
+ const {
+ onScroll,
+ showLeftArrow,
+ showRightArrow,
+ slideLeft,
+ slideRight,
+ slidesRef,
+ } = useMatchesSlider(matches)
+
+ if (isEmpty(matches)) return null
+
+ return (
+
+ {showLeftArrow && (
+
+ )}
+
+ {map(matches, (match) => )}
+
+ {showRightArrow && (
+
+ )}
+
+ )
+}
diff --git a/src/features/MatchesSlider/styled.tsx b/src/features/MatchesSlider/styled.tsx
new file mode 100644
index 00000000..b91a9396
--- /dev/null
+++ b/src/features/MatchesSlider/styled.tsx
@@ -0,0 +1,32 @@
+import styled from 'styled-components/macro'
+
+export const Wrapper = styled.div`
+ position: relative;
+ margin-bottom: 16px;
+`
+
+export const Slides = styled.ul`
+ display: flex;
+ overflow-x: auto;
+ scroll-behavior: smooth;
+
+ &::-webkit-scrollbar {
+ display: none;
+ }
+`
+
+export const Arrow = styled.div<{ type: 'arrowLeft' | 'arrowRight' }>`
+ position: absolute;
+ 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')});
+ cursor: pointer;
+ transform: translate(-50%, -50%);
+ z-index: 1;
+`
diff --git a/src/features/Search/styled.tsx b/src/features/Search/styled.tsx
index 90fc2fe3..19471512 100644
--- a/src/features/Search/styled.tsx
+++ b/src/features/Search/styled.tsx
@@ -59,6 +59,7 @@ export const Results = styled.div`
width: 448px;
max-height: 431px;
overflow-y: auto;
+ z-index: 1;
${сustomScrollbar}
`
diff --git a/src/features/UserFavorites/styled.tsx b/src/features/UserFavorites/styled.tsx
index 09af0011..cee89d04 100644
--- a/src/features/UserFavorites/styled.tsx
+++ b/src/features/UserFavorites/styled.tsx
@@ -8,6 +8,10 @@ import { TooltipBlockWrapper } from './TooltipBlock/styled'
export const StyledLink = styled(Link)``
export const UserSportFavWrapper = styled.div`
+ position: fixed;
+ left: 0;
+ top: 0;
+ bottom: 0;
width: 80px;
display: flex;
flex: 0 0 auto;
diff --git a/src/helpers/index.tsx b/src/helpers/index.tsx
index e37b5313..dcebcabb 100644
--- a/src/helpers/index.tsx
+++ b/src/helpers/index.tsx
@@ -7,3 +7,4 @@ export * from './getProfileUrl'
export * from './getSportColor'
export * from './getSportLexic'
export * from './handleImg'
+export * from './msToMinutesAndSeconds'
diff --git a/src/helpers/msToMinutesAndSeconds/__tests__/index.tsx b/src/helpers/msToMinutesAndSeconds/__tests__/index.tsx
new file mode 100644
index 00000000..7eb3e2e4
--- /dev/null
+++ b/src/helpers/msToMinutesAndSeconds/__tests__/index.tsx
@@ -0,0 +1,7 @@
+import { msToMinutesAndSeconds } from '..'
+
+describe('msToMinutesAndSeconds helper', () => {
+ it('returns correct formatted time', () => {
+ expect(msToMinutesAndSeconds(1000000)).toBe('16:40')
+ })
+})
diff --git a/src/helpers/msToMinutesAndSeconds/index.tsx b/src/helpers/msToMinutesAndSeconds/index.tsx
new file mode 100644
index 00000000..cb692132
--- /dev/null
+++ b/src/helpers/msToMinutesAndSeconds/index.tsx
@@ -0,0 +1,8 @@
+export const msToMinutesAndSeconds = (ms: number) => {
+ const minutes = Math.floor(ms / 60000)
+ const seconds = Number(((ms % 60000) / 1000).toFixed(0))
+
+ return seconds === 60
+ ? `${minutes + 1}:00`
+ : `${minutes}:${seconds < 10 ? '0' : ''}${seconds}`
+}
diff --git a/src/requests/getMatches.tsx b/src/requests/getMatches.tsx
index bbe5143c..47542339 100644
--- a/src/requests/getMatches.tsx
+++ b/src/requests/getMatches.tsx
@@ -30,13 +30,13 @@ export type Content = {
matches: Array,
name_eng: string,
name_rus: string,
- sport: number,
+ sport: 1 | 2 | 3,
}
export type Match = {
date: string,
id: number,
- round_id: number,
+ round_id: number | null,
stream_status: number,
team1: Team,
team2: Team,
@@ -53,7 +53,13 @@ type Args = {
date: string,
matchStatus: MatchStatuses,
sportType: SportTypes,
- tournamentId: number,
+ tournamentId: number | null,
+}
+
+export type Matches = {
+ broadcast: Array,
+ features: Array,
+ highlights: Array,
}
export const getMatches = async ({
@@ -79,5 +85,15 @@ export const getMatches = async ({
url: DATA_URL,
}).then(getResponseData(proc))
- return data.video_content.broadcast.content || []
+ const {
+ broadcast,
+ features,
+ highlights,
+ } = data.video_content
+
+ return {
+ broadcast: broadcast.content || [],
+ features: features.content || [],
+ highlights: highlights.content || [],
+ }
}