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 = () => ( + + <T9n t='video_from_my_subscriptions' /> + + ) 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 || [], + } }