Ott 563 match popup (#255)
* Ott 563 part 1/popup (#239) * feat(563): added images * feat(563): added MatchPopup * refactor(563): removed MatchHover components * Ott 563 part 2/components (#240) * feat(563): added components * feat(563): popup navigation * Ott 563 part 3/players list (#242) * feat(563): players list component * refactor(563): removed closeIcon svg and reused Close icon component with diff color * refactor(563): fix review comments * Update src/features/MatchPopup/components/PlayersList/index.tsx Co-authored-by: Serg <936x936@gmail.com> Co-authored-by: Serg <936x936@gmail.com> * feat(563): settings components (#244) * Ott 563 part 5/checkbox radio (#245) * refactor(563): swaping background icons to Icon components * fix(563): fixed components spacing * refactor(563): removed svg icon files * refactor(563): fix review comments * Ott 563 part 6/display match playlists (#248) * feat(563): added requests and lexics * feat(563): display match playlist buttons * refacotr(563): fixed pr comments * Ott 563 part 7/playlist format actions (#249) * refactor(563): lexics store lexic id update * feat(563): display playlist formats and actions * feat(563): display team players * refactor(563): moved type to types file * Ott 563 part 8/imprvs (#252) * fix(563): close on outside click * fix(563): restored full match click * feat(563): display full match duration * fix(563): scrollable players list and shirt number * fix(563): fix review comments * Ott 563 part 8/mobile desktop settings (#254) * fix(563): add mobile lexics * feat(563): wip * refactor(563): split Settings into Desktop and Mobile Co-authored-by: Serg <936x936@gmail.com>keep-around/af30b88d367751c9e05a735e4a0467a96238ef47
|
After Width: | Height: | Size: 444 B |
|
Before Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 860 B |
|
After Width: | Height: | Size: 211 KiB |
|
Before Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 853 B |
|
After Width: | Height: | Size: 4.2 KiB |
@ -0,0 +1,53 @@ |
||||
import styled, { css } from 'styled-components/macro' |
||||
|
||||
import { devices } from 'config' |
||||
|
||||
type SvgColorStylesProps = { |
||||
checked?: boolean, |
||||
} |
||||
|
||||
export const svgColorStyles = css<SvgColorStylesProps>` |
||||
fill: ${({ checked }) => (checked ? '#ffffff' : '#B8C1CC')}; |
||||
|
||||
@media ${devices.mobile} { |
||||
fill: ${({ checked }) => (checked ? '#294FC4' : '#B8C1CC')} |
||||
} |
||||
` |
||||
|
||||
export const CheckboxSvg = styled.svg` |
||||
margin-right: 22px; |
||||
|
||||
${svgColorStyles} |
||||
` |
||||
|
||||
type Props = { |
||||
checked?: boolean, |
||||
} |
||||
|
||||
export const Icon = ({ checked }: Props) => { |
||||
const id = checked ? '#checkbox-checked' : '#checkbox-unchecked' |
||||
return ( |
||||
<CheckboxSvg |
||||
width='24' |
||||
height='24' |
||||
viewBox='0 0 24 24' |
||||
checked={checked} |
||||
xmlns='http://www.w3.org/2000/svg' |
||||
> |
||||
<use href={id} /> |
||||
<defs> |
||||
<path |
||||
id='checkbox-checked' |
||||
fillRule='evenodd' |
||||
clipRule='evenodd' |
||||
d='M0.508636 2.54804C0 3.5463 0 4.85309 0 7.46667V15.8667C0 18.4802 0 19.787 0.508636 20.7853C0.956045 21.6634 1.66995 22.3773 2.54804 22.8247C3.5463 23.3333 4.85309 23.3333 7.46667 23.3333H15.8667C18.4802 23.3333 19.787 23.3333 20.7853 22.8247C21.6634 22.3773 22.3773 21.6634 22.8247 20.7853C23.3333 19.787 23.3333 18.4802 23.3333 15.8667V7.46667C23.3333 4.85309 23.3333 3.5463 22.8247 2.54804C22.3773 1.66995 21.6634 0.956045 20.7853 0.508636C19.787 0 18.4802 0 15.8667 0H7.46667C4.85309 0 3.5463 0 2.54804 0.508636C1.66995 0.956045 0.956045 1.66995 0.508636 2.54804ZM18.9083 8.40829C19.3639 7.95268 19.3639 7.21399 18.9083 6.75838C18.4527 6.30276 17.714 6.30276 17.2584 6.75838L9.33333 14.6834L6.07496 11.425C5.61935 10.9694 4.88065 10.9694 4.42504 11.425C3.96943 11.8807 3.96943 12.6193 4.42504 13.075L8.50838 17.1583C8.96399 17.6139 9.70268 17.6139 10.1583 17.1583L18.9083 8.40829Z' |
||||
/> |
||||
|
||||
<path |
||||
id='checkbox-unchecked' |
||||
d='M17.3509 0C19.4311 0 20.1855 0.216593 20.946 0.62331C21.7065 1.03003 22.3033 1.62687 22.71 2.38736C23.1167 3.14785 23.3333 3.90219 23.3333 5.9824V17.3509C23.3333 19.4311 23.1167 20.1855 22.71 20.946C22.3033 21.7065 21.7065 22.3033 20.946 22.71C20.1855 23.1167 19.4311 23.3333 17.3509 23.3333H5.9824C3.90219 23.3333 3.14785 23.1167 2.38736 22.71C1.62687 22.3033 1.03003 21.7065 0.62331 20.946C0.216593 20.1855 0 19.4311 0 17.3509V5.9824C0 3.90219 0.216593 3.14785 0.62331 2.38736C1.03003 1.62687 1.62687 1.03003 2.38736 0.62331C3.14785 0.216593 3.90219 0 5.9824 0H17.3509ZM18.0088 2.33333H5.32453C4.28443 2.33333 3.90726 2.44163 3.52701 2.64499C3.14677 2.84835 2.84835 3.14677 2.64499 3.52701C2.44163 3.90726 2.33333 4.28443 2.33333 5.32453V18.0088C2.33333 19.0489 2.44163 19.4261 2.64499 19.8063C2.84835 20.1866 3.14677 20.485 3.52701 20.6883C3.90726 20.8917 4.28443 21 5.32453 21H18.0088C19.0489 21 19.4261 20.8917 19.8063 20.6883C20.1866 20.485 20.485 20.1866 20.6883 19.8063C20.8917 19.4261 21 19.0489 21 18.0088V5.32453C21 4.28443 20.8917 3.90726 20.6883 3.52701C20.485 3.14677 20.1866 2.84835 19.8063 2.64499C19.4261 2.44163 19.0489 2.33333 18.0088 2.33333Z' |
||||
/> |
||||
</defs> |
||||
</CheckboxSvg> |
||||
) |
||||
} |
||||
@ -1,62 +1,27 @@ |
||||
import styled from 'styled-components/macro' |
||||
|
||||
import { devices } from 'config/devices' |
||||
|
||||
export const Wrapper = styled.div` |
||||
@media ${devices.tablet} { |
||||
position: absolute; |
||||
left: 0; |
||||
top: 0; |
||||
width: 162px; |
||||
height: 100px; |
||||
} |
||||
` |
||||
export const Wrapper = styled.span.attrs(() => ({ |
||||
role: 'checkbox', |
||||
tabIndex: 0, |
||||
}))`` |
||||
|
||||
export const Label = styled.label` |
||||
display: flex; |
||||
align-items: center; |
||||
color: ${({ theme: { colors } }) => colors.text}; |
||||
font-style: normal; |
||||
font-weight: bold; |
||||
font-size: 18px; |
||||
line-height: 21px; |
||||
cursor: pointer; |
||||
` |
||||
|
||||
export const Input = styled.input` |
||||
export const Input = styled.input.attrs(() => ({ |
||||
'aria-hidden': true, |
||||
tabIndex: -1, |
||||
type: 'checkbox', |
||||
}))` |
||||
position: absolute; |
||||
z-index: -1; |
||||
opacity: 0; |
||||
|
||||
&+${Label} { |
||||
display: inline-flex; |
||||
align-items: center; |
||||
user-select: none; |
||||
} |
||||
|
||||
&+${Label}::before { |
||||
content: ''; |
||||
display: inline-block; |
||||
width: 24px; |
||||
height: 24px; |
||||
margin-right: 22px; |
||||
background-repeat: no-repeat; |
||||
background-position: center center; |
||||
background-image: url(/images/checkboxUnchecked.svg); |
||||
cursor: pointer; |
||||
} |
||||
|
||||
&:checked+${Label}::before { |
||||
background-image: url(/images/checkboxChecked.svg); |
||||
} |
||||
|
||||
@media ${devices.tablet} { |
||||
&+${Label}::before { |
||||
width: 288px; |
||||
height: 100px; |
||||
background-image: none; |
||||
} |
||||
&:checked+${Label}::before { |
||||
background-image: none; |
||||
} |
||||
|
||||
} |
||||
` |
||||
|
||||
@ -1,18 +0,0 @@ |
||||
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; |
||||
` |
||||
@ -0,0 +1,39 @@ |
||||
import styled from 'styled-components/macro' |
||||
|
||||
import { svgColorStyles } from '../Checkbox/Icon' |
||||
|
||||
export const RadioSvg = styled.svg` |
||||
margin-right: 22px; |
||||
|
||||
${svgColorStyles} |
||||
` |
||||
|
||||
type Props = { |
||||
checked?: boolean, |
||||
} |
||||
|
||||
export const Icon = ({ checked }: Props) => { |
||||
const id = checked ? '#radio-checked' : '#radio-unchecked' |
||||
return ( |
||||
<RadioSvg |
||||
width='25' |
||||
height='26' |
||||
viewBox='0 0 25 26' |
||||
checked={checked} |
||||
xmlns='http://www.w3.org/2000/svg' |
||||
> |
||||
<use href={id} /> |
||||
<defs> |
||||
<path |
||||
id='radio-checked' |
||||
d='M12.375 0.75C19.2095 0.75 24.75 6.29048 24.75 13.125C24.75 19.9595 19.2095 25.5 12.375 25.5C5.54048 25.5 0 19.9595 0 13.125C0 6.29048 5.54048 0.75 12.375 0.75ZM12.375 3C6.78312 3 2.25 7.53312 2.25 13.125C2.25 18.7169 6.78312 23.25 12.375 23.25C17.9669 23.25 22.5 18.7169 22.5 13.125C22.5 7.53312 17.9669 3 12.375 3ZM12.375 5.25C16.7242 5.25 20.25 8.77576 20.25 13.125C20.25 17.4742 16.7242 21 12.375 21C8.02576 21 4.5 17.4742 4.5 13.125C4.5 8.77576 8.02576 5.25 12.375 5.25Z' |
||||
/> |
||||
|
||||
<path |
||||
id='radio-unchecked' |
||||
d='M12.375 0C19.2095 0 24.75 5.54048 24.75 12.375C24.75 19.2095 19.2095 24.75 12.375 24.75C5.54048 24.75 0 19.2095 0 12.375C0 5.54048 5.54048 0 12.375 0ZM12.375 2.25C6.78312 2.25 2.25 6.78312 2.25 12.375C2.25 17.9669 6.78312 22.5 12.375 22.5C17.9669 22.5 22.5 17.9669 22.5 12.375C22.5 6.78312 17.9669 2.25 12.375 2.25Z' |
||||
/> |
||||
</defs> |
||||
</RadioSvg> |
||||
) |
||||
} |
||||
@ -1,81 +1,27 @@ |
||||
import styled, { css } from 'styled-components/macro' |
||||
import styled from 'styled-components/macro' |
||||
|
||||
import { devices } from 'config/devices' |
||||
|
||||
type WrapperProps = { |
||||
isUserAccountPage?: boolean, |
||||
} |
||||
|
||||
export const Wrapper = styled.div<WrapperProps>` |
||||
|
||||
@media ${devices.tablet} { |
||||
${({ isUserAccountPage }) => (!isUserAccountPage |
||||
? css` |
||||
position: absolute; |
||||
left: 0; |
||||
top: 0; |
||||
width: 163px; |
||||
height: 100px; |
||||
border-radius: 10px; |
||||
` |
||||
: '')} |
||||
} |
||||
` |
||||
export const Wrapper = styled.span.attrs(() => ({ |
||||
role: 'radio', |
||||
tabIndex: 0, |
||||
}))`` |
||||
|
||||
export const Label = styled.label` |
||||
display: flex; |
||||
align-items: center; |
||||
color: ${({ theme: { colors } }) => colors.text}; |
||||
font-style: normal; |
||||
font-weight: bold; |
||||
font-size: 18px; |
||||
line-height: 21px; |
||||
cursor: pointer; |
||||
` |
||||
type InputProps = { |
||||
isUserAccountPage?: boolean, |
||||
} |
||||
|
||||
export const Input = styled.input<InputProps>` |
||||
export const Input = styled.input.attrs(() => ({ |
||||
'aria-hidden': true, |
||||
tabIndex: -1, |
||||
type: 'radio', |
||||
}))` |
||||
position: absolute; |
||||
z-index: -1; |
||||
opacity: 0; |
||||
|
||||
&+${Label} { |
||||
display: inline-flex; |
||||
align-items: center; |
||||
user-select: none; |
||||
} |
||||
|
||||
&+${Label}::before { |
||||
content: ''; |
||||
display: inline-block; |
||||
width: 26px; |
||||
height: 26px; |
||||
margin-right: 22px; |
||||
background-repeat: no-repeat; |
||||
background-position: center center; |
||||
background-image: url(/images/radioUnchecked.svg); |
||||
cursor: pointer; |
||||
} |
||||
|
||||
&:checked+${Label}::before { |
||||
background-image: url(/images/radioChecked.svg); |
||||
} |
||||
|
||||
@media ${devices.tablet} { |
||||
${({ isUserAccountPage }) => (!isUserAccountPage |
||||
? css` |
||||
&+${Label}::before { |
||||
width: 163px; |
||||
height: 100px; |
||||
border-radius: 10px; |
||||
margin-right: 0; |
||||
background-image: none; |
||||
} |
||||
|
||||
&:checked+${Label}::before { |
||||
background-image: none; |
||||
} |
||||
` |
||||
: '')} |
||||
} |
||||
|
||||
` |
||||
|
||||
@ -0,0 +1,14 @@ |
||||
export const Close = () => ( |
||||
<svg |
||||
width='16' |
||||
height='16' |
||||
viewBox='0 0 16 16' |
||||
fill='none' |
||||
xmlns='http://www.w3.org/2000/svg' |
||||
> |
||||
<path |
||||
d='M14.7097 1.29006C15.2077 1.78798 15.2077 2.59527 14.7097 3.09319L9.80326 7.99996L14.7097 12.9067C15.1662 13.3632 15.2042 14.0796 14.8239 14.5793L14.7097 14.7099C14.2118 15.2078 13.4045 15.2078 12.9066 14.7099L7.99984 9.80338L3.09307 14.7099C2.59515 15.2078 1.78786 15.2078 1.28994 14.7099C0.792024 14.212 0.792024 13.4047 1.28994 12.9067L6.19642 7.99996L1.28994 3.09319C0.833518 2.63676 0.795482 1.92038 1.17584 1.42063L1.28994 1.29006C1.78786 0.792146 2.59515 0.792146 3.09307 1.29006L7.99984 6.19654L12.9066 1.29006C13.4045 0.792146 14.2118 0.792146 14.7097 1.29006Z' |
||||
fill='currentColor' |
||||
/> |
||||
</svg> |
||||
) |
||||
@ -1,3 +1,3 @@ |
||||
export type LexicsId = string |
||||
export type LexicsId = string | number |
||||
|
||||
export type LexicsConfig = {[lexicsId: string]: number} |
||||
|
||||
@ -1,58 +0,0 @@ |
||||
import type { MouseEvent } from 'react' |
||||
|
||||
import { Link } from 'react-router-dom' |
||||
|
||||
import type { Match } from 'features/Matches' |
||||
import { OutsideClick } from 'features/OutsideClick' |
||||
|
||||
import { |
||||
CardHoverInner, |
||||
CardHoverTitle, |
||||
CardHoverWrapper, |
||||
MoreVideo, |
||||
Row, |
||||
Rows, |
||||
} from '../styled' |
||||
|
||||
type Props = { |
||||
match: Match, |
||||
onClose: () => void, |
||||
} |
||||
|
||||
const stopProp = (e: MouseEvent<HTMLDivElement>) => { |
||||
e.stopPropagation() |
||||
} |
||||
|
||||
export const CardBackside = ({ |
||||
match: { |
||||
id, |
||||
sportName, |
||||
}, |
||||
onClose, |
||||
}: Props) => ( |
||||
<OutsideClick onClick={onClose}> |
||||
<CardHoverWrapper onClick={onClose}> |
||||
<CardHoverInner> |
||||
<CardHoverTitle t='match_video' /> |
||||
<Rows onClick={stopProp}> |
||||
<Row> |
||||
<Link to={`/${sportName}/matches/${id}`}> |
||||
<MoreVideo t='full_game' /> |
||||
</Link> |
||||
<MoreVideo t='game_time' /> |
||||
</Row> |
||||
|
||||
<Row> |
||||
<MoreVideo t='highlights' /> |
||||
<MoreVideo t='goals' /> |
||||
<MoreVideo t='interview' /> |
||||
</Row> |
||||
|
||||
<Row> |
||||
<MoreVideo t='players_video' /> |
||||
</Row> |
||||
</Rows> |
||||
</CardHoverInner> |
||||
</CardHoverWrapper> |
||||
</OutsideClick> |
||||
) |
||||
@ -1,33 +1,31 @@ |
||||
import type { KeyboardEvent } from 'react' |
||||
import { useCallback } from 'react' |
||||
|
||||
import { useToggle } from 'hooks' |
||||
|
||||
import type { Match } from 'features/Matches' |
||||
import { useMatchPopupStore } from 'features/MatchPopup' |
||||
|
||||
export const useCard = (match: Match) => { |
||||
const { |
||||
close, |
||||
isOpen, |
||||
open, |
||||
} = useToggle() |
||||
const { openPopup, setMatch } = useMatchPopupStore() |
||||
|
||||
const flipCard = useCallback(() => { |
||||
const openMatchPopup = useCallback(() => { |
||||
if (match.isClickable) { |
||||
open() |
||||
setMatch(match) |
||||
openPopup() |
||||
} |
||||
}, [match, open]) |
||||
}, [ |
||||
match, |
||||
openPopup, |
||||
setMatch, |
||||
]) |
||||
|
||||
const onKeyPress = useCallback((e: KeyboardEvent<HTMLLIElement>) => { |
||||
if (e.key === 'Enter') { |
||||
flipCard() |
||||
openMatchPopup() |
||||
} |
||||
}, [flipCard]) |
||||
}, [openMatchPopup]) |
||||
|
||||
return { |
||||
close, |
||||
flipCard, |
||||
isOpen, |
||||
onKeyPress, |
||||
openMatchPopup, |
||||
} |
||||
} |
||||
|
||||
@ -0,0 +1,28 @@ |
||||
import styled from 'styled-components/macro' |
||||
|
||||
import { T9n } from 'features/T9n' |
||||
import { ButtonSolid } from 'features/Common' |
||||
|
||||
const Button = styled(ButtonSolid)` |
||||
position: absolute; |
||||
bottom: 46px; |
||||
left: 12px; |
||||
width: calc(100% - 24px); |
||||
height: 44px; |
||||
background-color: #294FC4; |
||||
border-radius: 5px; |
||||
font-weight: 600; |
||||
font-size: 17px; |
||||
line-height: 22px; |
||||
letter-spacing: -0.408px; |
||||
` |
||||
|
||||
type Props = { |
||||
onClick: () => void, |
||||
} |
||||
|
||||
export const ApplyButton = ({ onClick }: Props) => ( |
||||
<Button onClick={onClick}> |
||||
<T9n t='apply' /> |
||||
</Button> |
||||
) |
||||
@ -0,0 +1,16 @@ |
||||
import styled from 'styled-components/macro' |
||||
|
||||
import { useMatchPopupStore } from 'features/MatchPopup/store' |
||||
|
||||
import { BaseButton } from '../../styled' |
||||
|
||||
const Button = styled(BaseButton)` |
||||
background-image: url(/images/back-icon.svg); |
||||
` |
||||
|
||||
export const BackButton = () => { |
||||
const { goBack } = useMatchPopupStore() |
||||
return ( |
||||
<Button onClick={goBack} /> |
||||
) |
||||
} |
||||
@ -0,0 +1,13 @@ |
||||
import { Close } from 'features/Icons/Close' |
||||
import { useMatchPopupStore } from 'features/MatchPopup/store' |
||||
|
||||
import { BaseButton } from '../../styled' |
||||
|
||||
export const CloseButton = () => { |
||||
const { closePopup } = useMatchPopupStore() |
||||
return ( |
||||
<BaseButton onClick={closePopup}> |
||||
<Close /> |
||||
</BaseButton> |
||||
) |
||||
} |
||||
@ -0,0 +1,49 @@ |
||||
import type { ChangeEvent } from 'react' |
||||
import { useState } from 'react' |
||||
|
||||
import isNaN from 'lodash/isNaN' |
||||
|
||||
import type { EpisodeDuration } from '../../store/hooks/useSettingsState' |
||||
|
||||
const LIMITS = { |
||||
max: 30, |
||||
min: 1, |
||||
} |
||||
|
||||
const isValidDuration = (value: number) => ( |
||||
isFinite(value) |
||||
&& value >= LIMITS.min |
||||
&& value <= LIMITS.max |
||||
) |
||||
|
||||
export type Props = { |
||||
onChange: (duration: EpisodeDuration) => void, |
||||
value: EpisodeDuration, |
||||
} |
||||
|
||||
export const useInputHandlers = ({ onChange, value: initialValue }: Props) => { |
||||
const [duration, setDuration] = useState(initialValue) |
||||
const handleChange = (key: 'before' | 'after') => ( |
||||
(e: ChangeEvent<HTMLInputElement>) => { |
||||
const seconds = Number(e.target.value) |
||||
setDuration({ |
||||
...duration, |
||||
[key]: isNaN(seconds) ? '' : seconds, |
||||
}) |
||||
} |
||||
) |
||||
|
||||
const handleBlur = () => { |
||||
const { after, before } = duration |
||||
if (isValidDuration(before) && isValidDuration(after)) { |
||||
onChange(duration) |
||||
} else { |
||||
setDuration(initialValue) |
||||
} |
||||
} |
||||
return { |
||||
duration, |
||||
handleBlur, |
||||
handleChange, |
||||
} |
||||
} |
||||
@ -0,0 +1,44 @@ |
||||
import { BlockTitle } from 'features/MatchPopup/styled' |
||||
import { T9n } from 'features/T9n' |
||||
|
||||
import { |
||||
Wrapper, |
||||
InputsWrapper, |
||||
Input, |
||||
Label, |
||||
} from './styled' |
||||
import type { Props } from './hooks' |
||||
import { useInputHandlers } from './hooks' |
||||
|
||||
export const EpisodeDurationInputs = (props: Props) => { |
||||
const { |
||||
duration, |
||||
handleBlur, |
||||
handleChange, |
||||
} = useInputHandlers(props) |
||||
return ( |
||||
<Wrapper> |
||||
<BlockTitle> |
||||
<T9n t='episode_duration' /> |
||||
</BlockTitle> |
||||
<InputsWrapper> |
||||
<Label> |
||||
<T9n t='sec_before' /> |
||||
<Input |
||||
value={duration.before} |
||||
onChange={handleChange('before')} |
||||
onBlur={handleBlur} |
||||
/> |
||||
</Label> |
||||
<Label> |
||||
<T9n t='sec_after' /> |
||||
<Input |
||||
value={duration.after} |
||||
onChange={handleChange('after')} |
||||
onBlur={handleBlur} |
||||
/> |
||||
</Label> |
||||
</InputsWrapper> |
||||
</Wrapper> |
||||
) |
||||
} |
||||
@ -0,0 +1,55 @@ |
||||
import styled from 'styled-components/macro' |
||||
|
||||
import { devices } from 'config' |
||||
|
||||
export const Wrapper = styled.div` |
||||
display: flex; |
||||
flex-direction: column; |
||||
margin-top: 40px; |
||||
padding-left: 35px; |
||||
padding-right: 20px; |
||||
|
||||
@media ${devices.mobile} { |
||||
padding: 0 12px; |
||||
margin-top: 0; |
||||
} |
||||
` |
||||
|
||||
export const InputsWrapper = styled.div` |
||||
margin-top: 18px; |
||||
|
||||
@media ${devices.mobile} { |
||||
padding: 0 2px; |
||||
} |
||||
` |
||||
|
||||
export const Input = styled.input` |
||||
width: 38px; |
||||
height: 24px; |
||||
background-color: #FFFFFF; |
||||
box-shadow: inset 0px 1px 1px rgba(0, 0, 0, 0.25); |
||||
border: 0.2px solid #D0D0D0; |
||||
border-radius: 4px; |
||||
text-align: center; |
||||
margin: 0 15px; |
||||
|
||||
@media ${devices.mobile} { |
||||
color: #FFFFFF; |
||||
background-color: #3F3F3F; |
||||
border: none; |
||||
} |
||||
` |
||||
|
||||
export const Label = styled.label` |
||||
font-weight: normal; |
||||
font-size: 16px; |
||||
line-height: 24px; |
||||
letter-spacing: -0.01em; |
||||
color: rgba(255, 255, 255, 0.5); |
||||
|
||||
@media ${devices.mobile} { |
||||
line-height: 20px; |
||||
letter-spacing: 0.1px; |
||||
color: #FFFFFF; |
||||
} |
||||
` |
||||
@ -0,0 +1,73 @@ |
||||
import styled from 'styled-components/macro' |
||||
|
||||
import { devices } from 'config' |
||||
|
||||
const Card = styled.div.attrs({ |
||||
tabIndex: 0, |
||||
})` |
||||
width: 100%; |
||||
height: 100%; |
||||
border: 1px solid transparent; |
||||
transition: border 0.5s ease-out; |
||||
background: linear-gradient( |
||||
180deg, |
||||
rgba(255, 255, 255, 0.1) 0%, |
||||
rgba(255, 255, 255, 0) 100% |
||||
), |
||||
#5c5c5c; |
||||
box-shadow: 0px 2px 3px rgba(0, 0, 0, 0.3); |
||||
border-radius: 2px; |
||||
|
||||
cursor: pointer; |
||||
display: flex; |
||||
flex-direction: column; |
||||
justify-content: space-between; |
||||
align-items: stretch; |
||||
|
||||
&:hover { |
||||
border: 1px solid #A4A4A4; |
||||
} |
||||
|
||||
@media ${devices.mobile} { |
||||
background-color: #3F3F3F; |
||||
} |
||||
` |
||||
|
||||
const Image = styled.img` |
||||
width: 100%; |
||||
height: 145px; |
||||
object-fit: cover; |
||||
|
||||
@media ${devices.mobile} { |
||||
height: 225px;; |
||||
background-color: #3F3F3F; |
||||
} |
||||
` |
||||
|
||||
const Title = styled.span` |
||||
font-style: normal; |
||||
font-weight: 600; |
||||
font-size: 14px; |
||||
line-height: 16px; |
||||
padding: 0 20px; |
||||
flex-grow: 1; |
||||
display: flex; |
||||
align-items: center; |
||||
color: #fff; |
||||
|
||||
@media ${devices.mobile} { |
||||
padding: 12px; |
||||
} |
||||
` |
||||
|
||||
type Props = { |
||||
imgSrc: string, |
||||
title: string, |
||||
} |
||||
|
||||
export const InterviewCard = ({ imgSrc, title }: Props) => ( |
||||
<Card> |
||||
<Image src={imgSrc} alt={title} /> |
||||
<Title>{title}</Title> |
||||
</Card> |
||||
) |
||||
@ -0,0 +1,84 @@ |
||||
import styled from 'styled-components/macro' |
||||
|
||||
import { devices } from 'config' |
||||
|
||||
import { T9n } from 'features/T9n' |
||||
|
||||
import { InterviewCard } from '../InterviewCard' |
||||
import { BlockTitle } from '../../styled' |
||||
|
||||
const Wrapper = styled.div` |
||||
width: 100%; |
||||
display: flex; |
||||
flex-direction: column; |
||||
align-items: center; |
||||
` |
||||
|
||||
const List = styled.ul` |
||||
margin: 17px auto; |
||||
width: 860px; |
||||
display: flex; |
||||
flex-wrap: wrap; |
||||
|
||||
@media ${devices.mobile} { |
||||
width: 100%; |
||||
margin: 12px auto; |
||||
} |
||||
` |
||||
|
||||
const Item = styled.li` |
||||
width: 200px; |
||||
height: 200px; |
||||
margin-bottom: 15px; |
||||
|
||||
:not(:last-child) { |
||||
margin-right: 20px; |
||||
|
||||
@media ${devices.mobile} { |
||||
margin-right: 0px; |
||||
} |
||||
} |
||||
|
||||
@media ${devices.mobile} { |
||||
width: 100%; |
||||
height: 287px; |
||||
font-size: 14px; |
||||
line-height: 18px; |
||||
color: rgba(255, 255, 255, 0.5); |
||||
text-transform: none; |
||||
} |
||||
` |
||||
|
||||
export const Interviews = () => ( |
||||
<Wrapper> |
||||
<BlockTitle> |
||||
<T9n t='match_interviews' /> |
||||
</BlockTitle> |
||||
<List> |
||||
<Item> |
||||
<InterviewCard |
||||
title='Константин Андреевич Петров' |
||||
imgSrc='images/preview2.png' |
||||
/> |
||||
</Item> |
||||
<Item> |
||||
<InterviewCard |
||||
title='Константин Андреевич Петров' |
||||
imgSrc='images/preview2.png' |
||||
/> |
||||
</Item> |
||||
<Item> |
||||
<InterviewCard |
||||
title='Константин Андреевич Петров' |
||||
imgSrc='images/preview2.png' |
||||
/> |
||||
</Item> |
||||
<Item> |
||||
<InterviewCard |
||||
title='Константин Андреевич Петров' |
||||
imgSrc='images/preview2.png' |
||||
/> |
||||
</Item> |
||||
</List> |
||||
</Wrapper> |
||||
) |
||||
@ -0,0 +1,80 @@ |
||||
import { useLocation } from 'react-router-dom' |
||||
|
||||
import styled from 'styled-components/macro' |
||||
|
||||
import { devices, PAGES } from 'config' |
||||
import { getSportLexic } from 'helpers' |
||||
|
||||
import { useMatchPopupStore } from 'features/MatchPopup/store' |
||||
|
||||
import { PlaylistButton } from '../PlaylistButton' |
||||
|
||||
const List = styled.ul` |
||||
margin: 40px auto 24px auto; |
||||
width: 855px; |
||||
display: flex; |
||||
justify-content: space-between; |
||||
flex-wrap: wrap; |
||||
|
||||
@media ${devices.mobile} { |
||||
width: 100%; |
||||
margin: 12px auto; |
||||
padding: 0 12px; |
||||
} |
||||
` |
||||
|
||||
const Item = styled.li` |
||||
margin-bottom: 15px; |
||||
width: 420px; |
||||
height: 50px; |
||||
|
||||
@media ${devices.mobile} { |
||||
width: 100%; |
||||
height: 36px; |
||||
margin-bottom: 12px; |
||||
} |
||||
` |
||||
|
||||
export const MatchPlaylist = () => { |
||||
const { pathname, search } = useLocation() |
||||
const { match, matchPlaylists } = useMatchPopupStore() |
||||
|
||||
if (!match || !matchPlaylists) return null |
||||
|
||||
// не меняем url при клике ссылки временно
|
||||
// до добавления плейлистов для голов, обзоров
|
||||
const currentRoute = `${pathname}${search}` |
||||
const sport = getSportLexic(match.sportType) |
||||
return ( |
||||
<List> |
||||
<Item> |
||||
<PlaylistButton |
||||
to={`${sport}${PAGES.match}/${match.id}`} |
||||
title='match_playlist_full_game' |
||||
duration={matchPlaylists.fullMatchDuration} |
||||
/> |
||||
</Item> |
||||
<Item> |
||||
<PlaylistButton |
||||
to={currentRoute} |
||||
title='match_playlist_highlights' |
||||
duration={matchPlaylists.highlights.dur} |
||||
/> |
||||
</Item> |
||||
<Item> |
||||
<PlaylistButton |
||||
to={currentRoute} |
||||
title='match_playlist_ball_in_play' |
||||
duration={matchPlaylists.ball_in_play.dur} |
||||
/> |
||||
</Item> |
||||
<Item> |
||||
<PlaylistButton |
||||
to={currentRoute} |
||||
title='match_playlist_goals' |
||||
duration={matchPlaylists.goals.dur} |
||||
/> |
||||
</Item> |
||||
</List> |
||||
) |
||||
} |
||||
@ -0,0 +1,56 @@ |
||||
import includes from 'lodash/includes' |
||||
import filter from 'lodash/filter' |
||||
import map from 'lodash/map' |
||||
|
||||
import type { Actions } from 'requests' |
||||
|
||||
import { T9n } from 'features/T9n' |
||||
import { BlockTitle } from 'features/MatchPopup/styled' |
||||
|
||||
import type { SelectedActions } from '../../store/hooks/useSettingsState' |
||||
import { |
||||
Wrapper, |
||||
Checkbox, |
||||
List, |
||||
Item, |
||||
} from './styled' |
||||
|
||||
type Props = { |
||||
actions: Actions, |
||||
onActionClick: (actions: SelectedActions) => void, |
||||
selectedActions: SelectedActions, |
||||
} |
||||
|
||||
export const PlayerActions = ({ |
||||
actions, |
||||
onActionClick, |
||||
selectedActions, |
||||
}: Props) => { |
||||
const handleActionClick = (id: number) => { |
||||
const newSelectedActions = includes(selectedActions, id) |
||||
? filter(selectedActions, (actionId) => actionId !== id) |
||||
: [...selectedActions, id] |
||||
|
||||
onActionClick(newSelectedActions) |
||||
} |
||||
return ( |
||||
<Wrapper> |
||||
<BlockTitle> |
||||
<T9n t='selected_player_actions' /> |
||||
</BlockTitle> |
||||
<List> |
||||
{ |
||||
map(actions, (action) => ( |
||||
<Item key={action.id}> |
||||
<Checkbox |
||||
checked={includes(selectedActions, action.id)} |
||||
onChange={() => handleActionClick(action.id)} |
||||
labelLexic={action.lexic} |
||||
/> |
||||
</Item> |
||||
)) |
||||
} |
||||
</List> |
||||
</Wrapper> |
||||
) |
||||
} |
||||
@ -0,0 +1,102 @@ |
||||
import styled, { css } from 'styled-components/macro' |
||||
|
||||
import { devices } from 'config' |
||||
|
||||
import { Checkbox as BaseCheckbox } from 'features/Common' |
||||
import { Label } from 'features/Common/Checkbox/styled' |
||||
import { CheckboxSvg } from 'features/Common/Checkbox/Icon' |
||||
|
||||
export const Wrapper = styled.div` |
||||
display: flex; |
||||
flex-direction: column; |
||||
margin-top: 56px; |
||||
padding-left: 35px; |
||||
|
||||
@media ${devices.mobile} { |
||||
margin-top: 30px; |
||||
padding: 0 12px; |
||||
} |
||||
` |
||||
|
||||
const scrollBarStyles = css` |
||||
::-webkit-scrollbar { |
||||
width: 8px; |
||||
height: 8px; |
||||
} |
||||
|
||||
::-webkit-scrollbar-thumb { |
||||
background-color: rgba(196, 196, 196, 0.3); |
||||
border-radius: 3px; |
||||
} |
||||
|
||||
::-webkit-scrollbar-button { |
||||
display: none; |
||||
} |
||||
|
||||
::-webkit-scrollbar-track, |
||||
::-webkit-scrollbar-corner { |
||||
border-radius: 3px; |
||||
background: rgba(103, 103, 103, 0.3); |
||||
} |
||||
` |
||||
|
||||
export const List = styled.ul` |
||||
display: flex; |
||||
flex-wrap: wrap; |
||||
margin-top: 20px; |
||||
height: 262px; |
||||
overflow-y: auto; |
||||
|
||||
@media ${devices.mobile} { |
||||
height: 100%; |
||||
margin: 8px 0; |
||||
padding: 0 2px 100px 2px; |
||||
overflow-y: initial; |
||||
flex-direction: column; |
||||
} |
||||
|
||||
${scrollBarStyles} |
||||
` |
||||
|
||||
export const Item = styled.li` |
||||
width: calc(100% / 3); |
||||
|
||||
:not(:last-child) { |
||||
margin-bottom: 24px; |
||||
} |
||||
|
||||
@media ${devices.mobile} { |
||||
width: 100%; |
||||
height: 44px; |
||||
|
||||
:not(:last-child) { |
||||
margin-bottom: 0; |
||||
} |
||||
} |
||||
` |
||||
|
||||
export const Checkbox = styled(BaseCheckbox)` |
||||
height: 100%; |
||||
|
||||
${Label} { |
||||
color: #ffffff; |
||||
font-weight: normal; |
||||
font-size: 20px; |
||||
line-height: 21px; |
||||
|
||||
@media ${devices.mobile} { |
||||
font-size: 16px; |
||||
line-height: 20px; |
||||
letter-spacing: 0.1px; |
||||
} |
||||
} |
||||
|
||||
${CheckboxSvg} { |
||||
margin-right: 15px; |
||||
} |
||||
|
||||
@media ${devices.mobile} { |
||||
display: flex; |
||||
align-items: center; |
||||
} |
||||
` |
||||
@ -0,0 +1,46 @@ |
||||
import map from 'lodash/map' |
||||
|
||||
import { ProfileTypes, SportTypes } from 'config' |
||||
|
||||
import type { Players } from 'requests' |
||||
|
||||
import { Teams } from '../../types' |
||||
import { |
||||
List, |
||||
Item, |
||||
Logo, |
||||
PlayerName, |
||||
} from './styled' |
||||
|
||||
type Props = { |
||||
players: Players, |
||||
sportType: SportTypes, |
||||
team: Teams, |
||||
} |
||||
|
||||
export const PlayersList = ({ |
||||
players, |
||||
sportType, |
||||
team, |
||||
}: Props) => ( |
||||
<List team={team}> |
||||
{ |
||||
map(players, (player) => ( |
||||
<Item key={player.id}> |
||||
<Logo |
||||
id={player.id} |
||||
sportType={sportType} |
||||
profileType={ProfileTypes.PLAYERS} |
||||
team={team} |
||||
/> |
||||
<PlayerName |
||||
nameObj={{ |
||||
name_eng: `${player.num} ${player.name_eng}`, |
||||
name_rus: `${player.num} ${player.name_rus}`, |
||||
}} |
||||
/> |
||||
</Item> |
||||
)) |
||||
} |
||||
</List> |
||||
) |
||||
@ -0,0 +1,90 @@ |
||||
import styled from 'styled-components/macro' |
||||
|
||||
import { devices } from 'config' |
||||
|
||||
import { Name } from 'features/Name' |
||||
import { ProfileLogo } from 'features/ProfileLogo' |
||||
|
||||
import { Teams } from '../../types' |
||||
|
||||
type ListProps = { |
||||
team: Teams, |
||||
} |
||||
|
||||
export const List = styled.ul<ListProps>` |
||||
width: calc((100% - 30px) / 2); |
||||
height: 230px; |
||||
overflow: auto; |
||||
display: flex; |
||||
flex-wrap: wrap; |
||||
flex-direction: ${({ team }) => ( |
||||
team === Teams.TEAM1 |
||||
? 'row-reverse' |
||||
: 'row' |
||||
)}; |
||||
|
||||
@media ${devices.mobile} { |
||||
width: 100%; |
||||
height: auto; |
||||
flex-direction: column; |
||||
} |
||||
` |
||||
|
||||
export const Item = styled.li.attrs(() => ({ |
||||
tabIndex: 0, |
||||
}))` |
||||
width: 76px; |
||||
height: 95px; |
||||
margin: 10px; |
||||
display: flex; |
||||
flex-direction: column; |
||||
align-items: center; |
||||
justify-content: space-between; |
||||
cursor: pointer; |
||||
|
||||
@media ${devices.mobile} { |
||||
width: 100%; |
||||
height: 60px; |
||||
flex-direction: row; |
||||
justify-content: flex-start; |
||||
margin: 0px; |
||||
padding: 10px; |
||||
} |
||||
` |
||||
|
||||
type LogoProps = { |
||||
team: Teams, |
||||
} |
||||
|
||||
export const Logo = styled(ProfileLogo)<LogoProps>` |
||||
width: 65px; |
||||
height: 65px; |
||||
border-radius: 50%; |
||||
background-color: ${({ team }) => ( |
||||
team === Teams.TEAM1 |
||||
? '#EB5757' |
||||
: '#2F80ED' |
||||
)}; |
||||
|
||||
@media ${devices.mobile} { |
||||
width: 49px; |
||||
height: 49px; |
||||
margin-right: 11px; |
||||
} |
||||
` |
||||
|
||||
export const PlayerName = styled(Name)` |
||||
width: 100%; |
||||
font-weight: bold; |
||||
font-size: 10px; |
||||
line-height: 10px; |
||||
text-align: center; |
||||
letter-spacing: 0.02em; |
||||
|
||||
@media ${devices.mobile} { |
||||
text-align: start; |
||||
font-size: 16px; |
||||
line-height: 20px; |
||||
letter-spacing: 0.1px; |
||||
} |
||||
` |
||||
@ -0,0 +1,49 @@ |
||||
import styled from 'styled-components/macro' |
||||
|
||||
import { T9n } from 'features/T9n' |
||||
import { useMatchPopupStore } from 'features/MatchPopup' |
||||
|
||||
import { Teams } from '../../types' |
||||
import { BlockTitle } from '../../styled' |
||||
import { PlayersList } from '../PlayersList' |
||||
|
||||
const Wrapper = styled.div` |
||||
display: flex; |
||||
flex-direction: column; |
||||
align-items: center; |
||||
margin-top: 7px; |
||||
` |
||||
|
||||
const ListsWrapper = styled.div` |
||||
width: 100%; |
||||
margin-top: 10px; |
||||
display: flex; |
||||
flex-direction: row; |
||||
justify-content: space-between; |
||||
` |
||||
|
||||
export const PlayersListDesktop = () => { |
||||
const { match, matchPlaylists } = useMatchPopupStore() |
||||
|
||||
if (!match || !matchPlaylists) return null |
||||
|
||||
return ( |
||||
<Wrapper> |
||||
<BlockTitle> |
||||
<T9n t='team_players' /> |
||||
</BlockTitle> |
||||
<ListsWrapper> |
||||
<PlayersList |
||||
team={Teams.TEAM1} |
||||
players={matchPlaylists.players1} |
||||
sportType={match.sportType} |
||||
/> |
||||
<PlayersList |
||||
team={Teams.TEAM2} |
||||
players={matchPlaylists.players2} |
||||
sportType={match.sportType} |
||||
/> |
||||
</ListsWrapper> |
||||
</Wrapper> |
||||
) |
||||
} |
||||
@ -0,0 +1,52 @@ |
||||
import { useState } from 'react' |
||||
|
||||
import { T9n } from 'features/T9n' |
||||
import { Name } from 'features/Name' |
||||
import { useMatchPopupStore } from 'features/MatchPopup' |
||||
|
||||
import { Teams } from '../../types' |
||||
import { BlockTitle } from '../../styled' |
||||
import { PlayersList } from '../PlayersList' |
||||
import { |
||||
Wrapper, |
||||
Tabs, |
||||
Tab, |
||||
} from './styled' |
||||
|
||||
export const PlayersListMobile = () => { |
||||
const { match, matchPlaylists } = useMatchPopupStore() |
||||
const [selectedTeam, setSelectedTeam] = useState<Teams>(Teams.TEAM1) |
||||
|
||||
if (!match || !matchPlaylists) return null |
||||
|
||||
const players = selectedTeam === Teams.TEAM1 |
||||
? matchPlaylists.players1 |
||||
: matchPlaylists.players2 |
||||
|
||||
return ( |
||||
<Wrapper> |
||||
<BlockTitle> |
||||
<T9n t='team_players' /> |
||||
</BlockTitle> |
||||
<Tabs> |
||||
<Tab |
||||
selected={selectedTeam === Teams.TEAM1} |
||||
onClick={() => setSelectedTeam(Teams.TEAM1)} |
||||
> |
||||
<Name nameObj={match.team1} /> |
||||
</Tab> |
||||
<Tab |
||||
selected={selectedTeam === Teams.TEAM2} |
||||
onClick={() => setSelectedTeam(Teams.TEAM2)} |
||||
> |
||||
<Name nameObj={match.team2} /> |
||||
</Tab> |
||||
</Tabs> |
||||
<PlayersList |
||||
team={selectedTeam} |
||||
players={players} |
||||
sportType={match.sportType} |
||||
/> |
||||
</Wrapper> |
||||
) |
||||
} |
||||
@ -0,0 +1,47 @@ |
||||
import styled, { css } from 'styled-components/macro' |
||||
|
||||
export const Wrapper = styled.div` |
||||
display: flex; |
||||
flex-direction: column; |
||||
align-items: center; |
||||
margin-top: 8px; |
||||
` |
||||
|
||||
export const Tabs = styled.ul` |
||||
width: calc(100% - 24px); |
||||
margin: 20px 0 12px 0; |
||||
height: 32px; |
||||
display: flex; |
||||
border: 1px solid rgba(255, 255, 255, 0.5); |
||||
border-radius: 10px; |
||||
overflow: hidden; |
||||
` |
||||
|
||||
type TabProps = { |
||||
selected?: boolean, |
||||
} |
||||
|
||||
export const Tab = styled.li.attrs(() => ({ |
||||
tabIndex: 0, |
||||
}))<TabProps>` |
||||
width: 50%; |
||||
padding: 0 12px; |
||||
display: flex; |
||||
justify-content: center; |
||||
align-items: center; |
||||
font-weight: 500; |
||||
font-size: 14px; |
||||
line-height: 18px; |
||||
cursor: pointer; |
||||
|
||||
${({ selected }) => ( |
||||
selected |
||||
? css` |
||||
color: #000000; |
||||
background-color: #ffffff; |
||||
` |
||||
: css` |
||||
color: #ffffff; |
||||
` |
||||
)} |
||||
` |
||||
@ -0,0 +1,87 @@ |
||||
import type { MouseEvent } from 'react' |
||||
import { Link } from 'react-router-dom' |
||||
|
||||
import styled from 'styled-components/macro' |
||||
|
||||
import { devices } from 'config' |
||||
import { secondsToHms } from 'helpers' |
||||
|
||||
import { T9n } from 'features/T9n' |
||||
|
||||
const StyledLink = styled(Link)` |
||||
border: none; |
||||
|
||||
cursor: pointer; |
||||
display: flex; |
||||
justify-content: space-between; |
||||
align-items: center; |
||||
width: 100%; |
||||
height: 100%; |
||||
padding: 0 25px; |
||||
background: linear-gradient( |
||||
180deg, |
||||
rgba(255, 255, 255, 0.1) 0%, |
||||
rgba(255, 255, 255, 0) 100% |
||||
), |
||||
#5c5c5c; |
||||
box-shadow: 0px 1px 1px rgba(0, 0, 0, 0.3); |
||||
border-radius: 2px; |
||||
|
||||
:hover { |
||||
background-color: #555555; |
||||
} |
||||
|
||||
@media ${devices.mobile} { |
||||
justify-content: center; |
||||
border-radius: 5px; |
||||
} |
||||
` |
||||
|
||||
const Title = styled.span` |
||||
font-weight: 500; |
||||
font-size: 20px; |
||||
line-height: 50px; |
||||
letter-spacing: 0.03em; |
||||
text-transform: uppercase; |
||||
color: #ffffff; |
||||
|
||||
@media ${devices.mobile} { |
||||
font-size: 17px; |
||||
line-height: 16px; |
||||
margin-right: 16px; |
||||
text-transform: none; |
||||
} |
||||
` |
||||
|
||||
const Duration = styled(Title)` |
||||
font-weight: 300; |
||||
font-size: 24px; |
||||
letter-spacing: 0.05em; |
||||
|
||||
@media ${devices.mobile} { |
||||
font-size: 17px; |
||||
line-height: 16px; |
||||
text-transform: none; |
||||
} |
||||
` |
||||
|
||||
const stopPropagation = (e: MouseEvent<HTMLAnchorElement>) => e.stopPropagation() |
||||
|
||||
type Props = { |
||||
duration: number, |
||||
title: string, |
||||
to: string, |
||||
} |
||||
|
||||
export const PlaylistButton = ({ |
||||
duration, |
||||
title, |
||||
to, |
||||
}: Props) => ( |
||||
<StyledLink to={to} onClick={stopPropagation}> |
||||
<Title> |
||||
<T9n t={title} /> |
||||
</Title> |
||||
<Duration>{secondsToHms(duration)}</Duration> |
||||
</StyledLink> |
||||
) |
||||
@ -0,0 +1,52 @@ |
||||
import { T9n } from 'features/T9n' |
||||
import { PlayerPlaylistFormats } from 'features/MatchPopup/types' |
||||
import { BlockTitle } from 'features/MatchPopup/styled' |
||||
|
||||
import { |
||||
Wrapper, |
||||
List, |
||||
Item, |
||||
Radio, |
||||
} from './styled' |
||||
|
||||
type Props = { |
||||
onFormatSelect: (format: PlayerPlaylistFormats) => void, |
||||
selectedFormat: PlayerPlaylistFormats, |
||||
} |
||||
|
||||
export const PlaylistFormats = ({ |
||||
onFormatSelect, |
||||
selectedFormat, |
||||
}: Props) => ( |
||||
<Wrapper> |
||||
<BlockTitle> |
||||
<T9n t='playlist_format' /> |
||||
</BlockTitle> |
||||
<List> |
||||
<Item> |
||||
<Radio |
||||
name='playlist_formats' |
||||
labelLexic='playlist_format_all_match_time' |
||||
checked={selectedFormat === PlayerPlaylistFormats.ALL_MATCH_TIME} |
||||
onChange={() => onFormatSelect(PlayerPlaylistFormats.ALL_MATCH_TIME)} |
||||
/> |
||||
</Item> |
||||
<Item> |
||||
<Radio |
||||
name='playlist_formats' |
||||
labelLexic='playlist_format_all_actions' |
||||
checked={selectedFormat === PlayerPlaylistFormats.ALL_ACTIONS} |
||||
onChange={() => onFormatSelect(PlayerPlaylistFormats.ALL_ACTIONS)} |
||||
/> |
||||
</Item> |
||||
<Item> |
||||
<Radio |
||||
name='playlist_formats' |
||||
labelLexic='playlist_format_selected_acions' |
||||
checked={selectedFormat === PlayerPlaylistFormats.SELECTED_ACTIONS} |
||||
onChange={() => onFormatSelect(PlayerPlaylistFormats.SELECTED_ACTIONS)} |
||||
/> |
||||
</Item> |
||||
</List> |
||||
</Wrapper> |
||||
) |
||||
@ -0,0 +1,69 @@ |
||||
import styled from 'styled-components/macro' |
||||
|
||||
import { devices } from 'config' |
||||
|
||||
import { Radio as BaseRadio } from 'features/Common' |
||||
import { Label } from 'features/Common/Radio/styled' |
||||
import { RadioSvg } from 'features/Common/Radio/Icon' |
||||
|
||||
export const Wrapper = styled.div` |
||||
display: flex; |
||||
flex-direction: column; |
||||
margin-top: 62px; |
||||
padding-left: 35px; |
||||
padding-right: 20px; |
||||
|
||||
@media ${devices.mobile} { |
||||
margin-top: 14px; |
||||
padding: 0 12px; |
||||
} |
||||
` |
||||
|
||||
export const List = styled.ul` |
||||
display: flex; |
||||
flex-direction: column; |
||||
margin: 15px 0; |
||||
|
||||
@media ${devices.mobile} { |
||||
padding: 0 2px; |
||||
} |
||||
` |
||||
|
||||
export const Item = styled.li` |
||||
:not(:last-child) { |
||||
margin-bottom: 20px; |
||||
} |
||||
|
||||
@media ${devices.mobile} { |
||||
height: 44px; |
||||
|
||||
:not(:last-child) { |
||||
margin-bottom: 0; |
||||
} |
||||
} |
||||
` |
||||
|
||||
export const Radio = styled(BaseRadio)` |
||||
${Label} { |
||||
color: #ffffff; |
||||
font-weight: normal; |
||||
font-size: 20px; |
||||
line-height: 21px; |
||||
} |
||||
|
||||
${RadioSvg} { |
||||
margin-right: 15px; |
||||
} |
||||
|
||||
@media ${devices.mobile} { |
||||
width: 100%; |
||||
height: 100%; |
||||
|
||||
${Label} { |
||||
height: 100%; |
||||
font-size: 16px; |
||||
line-height: 20px; |
||||
letter-spacing: 0.1px; |
||||
} |
||||
} |
||||
` |
||||
@ -0,0 +1,69 @@ |
||||
import { Fragment } from 'react' |
||||
|
||||
import { Name } from 'features/Name' |
||||
import { MediaQuery } from 'features/MediaQuery' |
||||
import { useMatchPopupStore } from 'features/MatchPopup' |
||||
|
||||
import { SettingsButton } from '../SettingsButton' |
||||
import { CloseButton } from '../CloseButton' |
||||
import { BackButton } from '../BackButton' |
||||
import { MatchPlaylist } from '../MatchPlaylist' |
||||
import { Interviews } from '../Interviews' |
||||
import { PlayersListDesktop } from '../PlayersListDesktop' |
||||
import { PlayersListMobile } from '../PlayersListMobile' |
||||
import { |
||||
Content, |
||||
Header, |
||||
HeaderActions, |
||||
HeaderTitle, |
||||
} from '../../styled' |
||||
|
||||
export const PlaylistPage = () => { |
||||
const { match, matchPlaylists } = useMatchPopupStore() |
||||
if (!match) return null |
||||
|
||||
const { team1, team2 } = match |
||||
const score = ` ${team1.score}:${team2.score} ` |
||||
return ( |
||||
<Content> |
||||
<Header> |
||||
<MediaQuery maxDevice='mobile'> |
||||
<HeaderActions position='left'> |
||||
<BackButton /> |
||||
</HeaderActions> |
||||
</MediaQuery> |
||||
|
||||
<HeaderTitle> |
||||
<Name nameObj={team1} /> |
||||
{score} |
||||
<Name nameObj={team2} /> |
||||
</HeaderTitle> |
||||
|
||||
<HeaderActions position='right'> |
||||
<SettingsButton /> |
||||
<MediaQuery minDevice='tablet'> |
||||
<CloseButton /> |
||||
</MediaQuery> |
||||
</HeaderActions> |
||||
</Header> |
||||
|
||||
{ |
||||
matchPlaylists && ( |
||||
<Fragment> |
||||
<MatchPlaylist /> |
||||
|
||||
<Interviews /> |
||||
|
||||
<MediaQuery maxDevice='mobile'> |
||||
<PlayersListMobile /> |
||||
</MediaQuery> |
||||
|
||||
<MediaQuery minDevice='tablet'> |
||||
<PlayersListDesktop /> |
||||
</MediaQuery> |
||||
</Fragment> |
||||
) |
||||
} |
||||
</Content> |
||||
) |
||||
} |
||||
@ -0,0 +1,16 @@ |
||||
import styled from 'styled-components/macro' |
||||
|
||||
import { useMatchPopupStore } from 'features/MatchPopup/store' |
||||
|
||||
import { BaseButton } from '../../styled' |
||||
|
||||
const Button = styled(BaseButton)` |
||||
background-image: url(/images/settings.svg); |
||||
` |
||||
|
||||
export const SettingsButton = () => { |
||||
const { goToSettings } = useMatchPopupStore() |
||||
return ( |
||||
<Button onClick={goToSettings} /> |
||||
) |
||||
} |
||||
@ -0,0 +1,39 @@ |
||||
import { Fragment } from 'react' |
||||
|
||||
import { useMatchPopupStore } from 'features/MatchPopup' |
||||
|
||||
import { PlaylistFormats } from '../PlaylistFormats' |
||||
import { EpisodeDurationInputs } from '../EpisodeDurationInputs' |
||||
import { PlayerActions } from '../PlayerActions' |
||||
import { PlayerPlaylistFormats } from '../../types' |
||||
|
||||
export const SettingsDesktop = () => { |
||||
const { |
||||
actions, |
||||
episodeDuration, |
||||
onActionClick, |
||||
onDurationChange, |
||||
onFormatSelect, |
||||
selectedActions, |
||||
selectedPlaylistFormat, |
||||
} = useMatchPopupStore() |
||||
return ( |
||||
<Fragment> |
||||
<PlaylistFormats |
||||
selectedFormat={selectedPlaylistFormat} |
||||
onFormatSelect={onFormatSelect} |
||||
/> |
||||
<EpisodeDurationInputs |
||||
value={episodeDuration} |
||||
onChange={onDurationChange} |
||||
/> |
||||
{selectedPlaylistFormat === PlayerPlaylistFormats.SELECTED_ACTIONS && ( |
||||
<PlayerActions |
||||
actions={actions} |
||||
onActionClick={onActionClick} |
||||
selectedActions={selectedActions} |
||||
/> |
||||
)} |
||||
</Fragment> |
||||
) |
||||
} |
||||
@ -0,0 +1,38 @@ |
||||
import { useState } from 'react' |
||||
|
||||
import { useMatchPopupStore } from 'features/MatchPopup' |
||||
|
||||
export const useMobileSettings = () => { |
||||
const { |
||||
actions, |
||||
episodeDuration: initialDuration, |
||||
goBack, |
||||
onActionClick, |
||||
onDurationChange, |
||||
onFormatSelect, |
||||
selectedActions: initialActions, |
||||
selectedPlaylistFormat: initialFormat, |
||||
} = useMatchPopupStore() |
||||
|
||||
const [selectedPlaylistFormat, setSelectedPlaylistFormat] = useState(initialFormat) |
||||
const [episodeDuration, setEpisodeDuration] = useState(initialDuration) |
||||
const [selectedActions, setSelectedActions] = useState(initialActions) |
||||
|
||||
const applySettings = () => { |
||||
onFormatSelect(selectedPlaylistFormat) |
||||
onDurationChange(episodeDuration) |
||||
onActionClick(selectedActions) |
||||
goBack() |
||||
} |
||||
|
||||
return { |
||||
actions, |
||||
applySettings, |
||||
episodeDuration, |
||||
selectedActions, |
||||
selectedPlaylistFormat, |
||||
setEpisodeDuration, |
||||
setSelectedActions, |
||||
setSelectedPlaylistFormat, |
||||
} |
||||
} |
||||
@ -0,0 +1,41 @@ |
||||
import { Fragment } from 'react' |
||||
|
||||
import { PlaylistFormats } from '../PlaylistFormats' |
||||
import { EpisodeDurationInputs } from '../EpisodeDurationInputs' |
||||
import { PlayerActions } from '../PlayerActions' |
||||
import { ApplyButton } from '../ApplyButton' |
||||
import { PlayerPlaylistFormats } from '../../types' |
||||
import { useMobileSettings } from './hooks' |
||||
|
||||
export const SettingsMobile = () => { |
||||
const { |
||||
actions, |
||||
applySettings, |
||||
episodeDuration, |
||||
selectedActions, |
||||
selectedPlaylistFormat, |
||||
setEpisodeDuration, |
||||
setSelectedActions, |
||||
setSelectedPlaylistFormat, |
||||
} = useMobileSettings() |
||||
return ( |
||||
<Fragment> |
||||
<PlaylistFormats |
||||
selectedFormat={selectedPlaylistFormat} |
||||
onFormatSelect={setSelectedPlaylistFormat} |
||||
/> |
||||
<EpisodeDurationInputs |
||||
value={episodeDuration} |
||||
onChange={setEpisodeDuration} |
||||
/> |
||||
{selectedPlaylistFormat === PlayerPlaylistFormats.SELECTED_ACTIONS && ( |
||||
<PlayerActions |
||||
actions={actions} |
||||
onActionClick={setSelectedActions} |
||||
selectedActions={selectedActions} |
||||
/> |
||||
)} |
||||
<ApplyButton onClick={applySettings} /> |
||||
</Fragment> |
||||
) |
||||
} |
||||
@ -0,0 +1,60 @@ |
||||
import styled from 'styled-components/macro' |
||||
|
||||
import { MediaQuery } from 'features/MediaQuery' |
||||
import { T9n } from 'features/T9n' |
||||
|
||||
import { CloseButton } from '../CloseButton' |
||||
import { BackButton } from '../BackButton' |
||||
import { SettingsDesktop } from '../SettingsDesktop' |
||||
import { SettingsMobile } from '../SettingsMobile' |
||||
import { |
||||
Content, |
||||
Header, |
||||
HeaderActions, |
||||
HeaderTitle, |
||||
} from '../../styled' |
||||
|
||||
const ButtonLabel = styled(T9n)` |
||||
display: flex; |
||||
align-items: center; |
||||
font-weight: normal; |
||||
font-size: 18px; |
||||
line-height: 21px; |
||||
color: rgba(255, 255, 255, 0.5); |
||||
` |
||||
|
||||
export const SettingsPage = () => ( |
||||
<Content> |
||||
<Header> |
||||
<HeaderActions |
||||
position='left' |
||||
marginLeft={15} |
||||
> |
||||
<BackButton /> |
||||
<MediaQuery minDevice='tablet'> |
||||
<ButtonLabel t='go_back_to_match' /> |
||||
</MediaQuery> |
||||
</HeaderActions> |
||||
|
||||
<MediaQuery maxDevice='mobile'> |
||||
<HeaderTitle> |
||||
<T9n t='match_settings' /> |
||||
</HeaderTitle> |
||||
</MediaQuery> |
||||
|
||||
<MediaQuery minDevice='tablet'> |
||||
<HeaderActions position='right'> |
||||
<CloseButton /> |
||||
</HeaderActions> |
||||
</MediaQuery> |
||||
</Header> |
||||
|
||||
<MediaQuery minDevice='tablet'> |
||||
<SettingsDesktop /> |
||||
</MediaQuery> |
||||
|
||||
<MediaQuery maxDevice='mobile'> |
||||
<SettingsMobile /> |
||||
</MediaQuery> |
||||
</Content> |
||||
) |
||||
@ -0,0 +1,4 @@ |
||||
export enum Teams { |
||||
TEAM1, |
||||
TEAM2, |
||||
} |
||||
@ -0,0 +1,49 @@ |
||||
import { Fragment } from 'react' |
||||
|
||||
import { Background } from 'features/Background' |
||||
import { MediaQuery } from 'features/MediaQuery' |
||||
import { useMatchPopupStore } from 'features/MatchPopup' |
||||
|
||||
import { SettingsPage } from './components/SettingsPage' |
||||
import { PlaylistPage } from './components/PlaylistPage' |
||||
import { PopupPages } from './types' |
||||
import { Modal } from './styled' |
||||
|
||||
export * from './store' |
||||
|
||||
export const MatchPopup = () => { |
||||
const { |
||||
closePopup, |
||||
isOpen, |
||||
page, |
||||
} = useMatchPopupStore() |
||||
|
||||
const pageElement = page === PopupPages.PLAYLIST |
||||
? <PlaylistPage /> |
||||
: <SettingsPage /> |
||||
|
||||
return ( |
||||
<Fragment> |
||||
<MediaQuery minDevice='tablet'> |
||||
<Modal |
||||
close={closePopup} |
||||
isOpen={isOpen} |
||||
withCloseButton={false} |
||||
> |
||||
{pageElement} |
||||
</Modal> |
||||
</MediaQuery> |
||||
|
||||
<MediaQuery maxDevice='mobile'> |
||||
<Modal |
||||
isOpen={isOpen} |
||||
withCloseButton={false} |
||||
> |
||||
<Background> |
||||
{pageElement} |
||||
</Background> |
||||
</Modal> |
||||
</MediaQuery> |
||||
</Fragment> |
||||
) |
||||
} |
||||
@ -0,0 +1,102 @@ |
||||
import { useState, useEffect } from 'react' |
||||
|
||||
import isEmpty from 'lodash/isEmpty' |
||||
|
||||
import type { MatchPlaylists } from 'requests' |
||||
import { getMatchPlaylists } from 'requests' |
||||
|
||||
import { useSettingsState } from './useSettingsState' |
||||
import { useSportActions } from './useSportActions' |
||||
import { usePopupNavigation } from './usePopupNavigation' |
||||
|
||||
import type { MatchData } from '../../types' |
||||
import { PopupPages, PlayerPlaylistFormats } from '../../types' |
||||
|
||||
export const useMatchPopup = () => { |
||||
const [match, setMatch] = useState<MatchData>(null) |
||||
const [matchPlaylists, setMatchPlaylists] = useState<MatchPlaylists | null>(null) |
||||
const { |
||||
closePopup, |
||||
goBack, |
||||
goToSettings, |
||||
isOpen, |
||||
openPopup, |
||||
page, |
||||
} = usePopupNavigation() |
||||
|
||||
const { |
||||
episodeDuration, |
||||
resetSelectedActions, |
||||
selectedActions, |
||||
selectedPlaylistFormat, |
||||
setEpisodeDuration, |
||||
setSelectedActions, |
||||
setSelectedPlaylistFormat, |
||||
} = useSettingsState(match?.sportType) |
||||
|
||||
const { actions, fetchSportActions } = useSportActions(match?.sportType) |
||||
|
||||
useEffect(() => { |
||||
if (!isOpen) { |
||||
setMatch(null) |
||||
setMatchPlaylists(null) |
||||
} |
||||
}, [isOpen]) |
||||
|
||||
useEffect(() => { |
||||
if (selectedPlaylistFormat !== PlayerPlaylistFormats.SELECTED_ACTIONS) { |
||||
resetSelectedActions() |
||||
} |
||||
}, [selectedPlaylistFormat, resetSelectedActions]) |
||||
|
||||
useEffect(() => { |
||||
const isSettingsPage = page === PopupPages.SETTINGS |
||||
const actionsFormatSelected = ( |
||||
selectedPlaylistFormat === PlayerPlaylistFormats.SELECTED_ACTIONS |
||||
) |
||||
if (isSettingsPage && actionsFormatSelected) { |
||||
fetchSportActions() |
||||
} |
||||
}, [ |
||||
selectedPlaylistFormat, |
||||
match, |
||||
page, |
||||
fetchSportActions, |
||||
resetSelectedActions, |
||||
]) |
||||
|
||||
useEffect(() => { |
||||
if (!match || !isOpen || page !== PopupPages.PLAYLIST) return |
||||
|
||||
getMatchPlaylists({ |
||||
matchId: match.id, |
||||
// запрос с экшнами [1, 2, 3] временный
|
||||
selectedActions: isEmpty(selectedActions) ? [1, 2, 3] : selectedActions, |
||||
sportType: match.sportType, |
||||
}).then(setMatchPlaylists) |
||||
}, [ |
||||
isOpen, |
||||
match, |
||||
page, |
||||
selectedActions, |
||||
]) |
||||
|
||||
return { |
||||
actions, |
||||
closePopup, |
||||
episodeDuration, |
||||
goBack, |
||||
goToSettings, |
||||
isOpen, |
||||
match, |
||||
matchPlaylists, |
||||
onActionClick: setSelectedActions, |
||||
onDurationChange: setEpisodeDuration, |
||||
onFormatSelect: setSelectedPlaylistFormat, |
||||
openPopup, |
||||
page, |
||||
selectedActions, |
||||
selectedPlaylistFormat, |
||||
setMatch, |
||||
} |
||||
} |
||||
@ -0,0 +1,44 @@ |
||||
import type { MouseEvent } from 'react' |
||||
import { useState, useCallback } from 'react' |
||||
|
||||
import { useToggle } from 'hooks' |
||||
|
||||
import { PopupPages } from '../../types' |
||||
|
||||
export const usePopupNavigation = () => { |
||||
const { |
||||
close, |
||||
isOpen, |
||||
open, |
||||
} = useToggle() |
||||
|
||||
const [page, setPage] = useState<PopupPages>(PopupPages.PLAYLIST) |
||||
|
||||
const closePopup = useCallback(() => { |
||||
close() |
||||
setPage(PopupPages.PLAYLIST) |
||||
}, [close]) |
||||
|
||||
const goBack = useCallback((e?: MouseEvent<HTMLElement>) => { |
||||
e?.stopPropagation() |
||||
if (page === PopupPages.PLAYLIST) { |
||||
closePopup() |
||||
} else { |
||||
setPage(PopupPages.PLAYLIST) |
||||
} |
||||
}, [page, closePopup]) |
||||
|
||||
const goToSettings = useCallback((e: MouseEvent<HTMLElement>) => { |
||||
e.stopPropagation() |
||||
setPage(PopupPages.SETTINGS) |
||||
}, []) |
||||
|
||||
return { |
||||
closePopup, |
||||
goBack, |
||||
goToSettings, |
||||
isOpen, |
||||
openPopup: open, |
||||
page, |
||||
} |
||||
} |
||||
@ -0,0 +1,92 @@ |
||||
import { useCallback, useMemo } from 'react' |
||||
|
||||
import isObject from 'lodash/isObject' |
||||
|
||||
import { SportTypes } from 'config' |
||||
|
||||
import { useLocalStore } from 'hooks' |
||||
|
||||
import { PlayerPlaylistFormats } from '../../types' |
||||
|
||||
export type SelectedActions = Array<number> |
||||
export type EpisodeDuration = { |
||||
after: number, |
||||
before: number, |
||||
} |
||||
type Settings = { |
||||
episodeDuration: EpisodeDuration, |
||||
selectedActions: SelectedActions, |
||||
selectedFormat: PlayerPlaylistFormats, |
||||
} |
||||
type SettingsBySport = Partial<Record<SportTypes, Settings>> |
||||
|
||||
const selectedActionsKey = 'playlist_settings' |
||||
const defaultSettings: Settings = { |
||||
episodeDuration: { |
||||
after: 6, |
||||
before: 6, |
||||
}, |
||||
selectedActions: [], |
||||
selectedFormat: PlayerPlaylistFormats.ALL_MATCH_TIME, |
||||
} |
||||
const validator = (value: unknown) => Boolean(value) && isObject(value) |
||||
|
||||
export const useSettingsState = (sportType?: SportTypes) => { |
||||
const [settingsObj, setSettingsObj] = useLocalStore<SettingsBySport>({ |
||||
defaultValue: {}, |
||||
key: selectedActionsKey, |
||||
validator, |
||||
}) |
||||
|
||||
/** |
||||
* Сетит настройки определенного вида спорта, |
||||
* работает как setState классовых компонентов, |
||||
* то что передается в сеттер мержит со стейтом |
||||
*/ |
||||
const setSettings = useCallback((newSettings: Partial<Settings>) => { |
||||
if (!sportType) return |
||||
setSettingsObj((state) => { |
||||
const oldSettings = state[sportType] || defaultSettings |
||||
return { |
||||
...state, |
||||
[sportType]: { ...oldSettings, ...newSettings }, |
||||
} |
||||
}) |
||||
}, [sportType, setSettingsObj]) |
||||
|
||||
const getSettings = useCallback(() => { |
||||
if (!sportType) return defaultSettings |
||||
return settingsObj[sportType] || defaultSettings |
||||
}, [settingsObj, sportType]) |
||||
|
||||
const setSelectedPlaylistFormat = useCallback( |
||||
(value: PlayerPlaylistFormats) => setSettings({ selectedFormat: value }), |
||||
[setSettings], |
||||
) |
||||
|
||||
const setSelectedActions = useCallback( |
||||
(value: SelectedActions) => setSettings({ selectedActions: value }), |
||||
[setSettings], |
||||
) |
||||
|
||||
const setEpisodeDuration = useCallback( |
||||
(value: EpisodeDuration) => setSettings({ episodeDuration: value }), |
||||
[setSettings], |
||||
) |
||||
|
||||
const resetSelectedActions = useCallback(() => { |
||||
setSelectedActions([]) |
||||
}, [setSelectedActions]) |
||||
|
||||
const settings = useMemo(getSettings, [getSettings]) |
||||
|
||||
return { |
||||
episodeDuration: settings.episodeDuration, |
||||
resetSelectedActions, |
||||
selectedActions: settings.selectedActions, |
||||
selectedPlaylistFormat: settings.selectedFormat, |
||||
setEpisodeDuration, |
||||
setSelectedActions, |
||||
setSelectedPlaylistFormat, |
||||
} |
||||
} |
||||
@ -0,0 +1,26 @@ |
||||
import { useCallback, useState } from 'react' |
||||
|
||||
import map from 'lodash/map' |
||||
|
||||
import { SportTypes } from 'config' |
||||
|
||||
import type { Actions } from 'requests' |
||||
import { getSportActions } from 'requests' |
||||
|
||||
import { useLexicsStore } from 'features/LexicsStore' |
||||
|
||||
export const useSportActions = (sportType?: SportTypes) => { |
||||
const [actions, setActions] = useState<Actions>([]) |
||||
const { addLexicsConfig } = useLexicsStore() |
||||
|
||||
const fetchSportActions = useCallback(() => { |
||||
if (!sportType) return |
||||
|
||||
getSportActions(sportType).then((sportActions) => { |
||||
setActions(sportActions) |
||||
addLexicsConfig(map(sportActions, ({ lexic }) => lexic)) |
||||
}) |
||||
}, [sportType, addLexicsConfig]) |
||||
|
||||
return { actions, fetchSportActions } |
||||
} |
||||
@ -0,0 +1,20 @@ |
||||
import type { ReactNode } from 'react' |
||||
import { createContext, useContext } from 'react' |
||||
|
||||
import { useMatchPopup } from './hooks' |
||||
|
||||
type Context = ReturnType<typeof useMatchPopup> |
||||
type Props = { children: ReactNode } |
||||
|
||||
const MatchPopupContext = createContext({} as Context) |
||||
|
||||
export const MatchPopupStore = ({ children }: Props) => { |
||||
const value = useMatchPopup() |
||||
return ( |
||||
<MatchPopupContext.Provider value={value}> |
||||
{children} |
||||
</MatchPopupContext.Provider> |
||||
) |
||||
} |
||||
|
||||
export const useMatchPopupStore = () => useContext(MatchPopupContext) |
||||
@ -0,0 +1,140 @@ |
||||
import styled, { css } from 'styled-components/macro' |
||||
|
||||
import { devices } from 'config' |
||||
|
||||
import { Modal as BaseModal } from 'features/Modal' |
||||
import { ModalWindow } from 'features/Modal/styled' |
||||
import { customScrollbar } from 'features/Common' |
||||
|
||||
export const Modal = styled(BaseModal)` |
||||
background-color: rgba(0, 0, 0, 0.7); |
||||
|
||||
${ModalWindow} { |
||||
width: 1222px; |
||||
height: 818px; |
||||
padding: 20px 0; |
||||
background-color: #3F3F3F; |
||||
border-radius: 5px; |
||||
|
||||
@media ${devices.mobile} { |
||||
width: 100vw; |
||||
height: 100vh; |
||||
padding: 0; |
||||
background-color: transparent; |
||||
} |
||||
} |
||||
` |
||||
|
||||
export const BaseButton = styled.button` |
||||
padding: 0; |
||||
border: none; |
||||
background: none; |
||||
|
||||
cursor: pointer; |
||||
width: 34px; |
||||
height: 34px; |
||||
color: white; |
||||
background-color: rgba(255, 255, 255, 0.12); |
||||
background-position: center; |
||||
background-repeat: no-repeat; |
||||
border-radius: 50%; |
||||
|
||||
:hover { |
||||
background-color: rgba(255, 255, 255, 0.22); |
||||
} |
||||
|
||||
@media ${devices.mobile} { |
||||
width: 24px; |
||||
height: 24px; |
||||
background-color: transparent; |
||||
border-radius: 0; |
||||
} |
||||
` |
||||
|
||||
export const Content = styled.div` |
||||
width: 100%; |
||||
height: 100%; |
||||
overflow-y: auto; |
||||
|
||||
${customScrollbar} |
||||
|
||||
@media ${devices.mobile} { |
||||
height: 100vh; |
||||
background-color: transparent; |
||||
} |
||||
` |
||||
|
||||
export const Header = styled.div` |
||||
position: relative; |
||||
height: 35px; |
||||
display: flex; |
||||
align-items: center; |
||||
|
||||
@media ${devices.mobile} { |
||||
height: 52px; |
||||
background-color: rgba(255, 255, 255, 0.1); |
||||
padding: 0 12px; |
||||
} |
||||
` |
||||
|
||||
type HeaderActionsProps = { |
||||
marginLeft?: number, |
||||
position: 'left' | 'right', |
||||
} |
||||
|
||||
export const HeaderActions = styled.div<HeaderActionsProps>` |
||||
position: absolute; |
||||
display: flex; |
||||
|
||||
${({ marginLeft = 0, position }) => css` |
||||
${position}: 20px; |
||||
margin-left: ${marginLeft}px; |
||||
`}
|
||||
|
||||
@media ${devices.mobile} { |
||||
${({ position }) => css` |
||||
${position}: 12px; |
||||
margin-left: 0; |
||||
`}
|
||||
} |
||||
|
||||
${BaseButton}:not(:last-child) { |
||||
margin-right: 20px; |
||||
} |
||||
` |
||||
|
||||
export const HeaderTitle = styled.h2` |
||||
position: absolute; |
||||
width: 70%; |
||||
left: 50%; |
||||
transform: translateX(-50%); |
||||
|
||||
font-weight: 600; |
||||
font-size: 24px; |
||||
line-height: 42px; |
||||
color: #FFFFFF; |
||||
text-align: center; |
||||
|
||||
@media ${devices.mobile} { |
||||
font-size: 19px; |
||||
line-height: 28px; |
||||
text-align: center; |
||||
text-overflow: ellipsis; |
||||
white-space: nowrap; |
||||
overflow: hidden; |
||||
} |
||||
` |
||||
|
||||
export const BlockTitle = styled.h3` |
||||
font-weight: normal; |
||||
font-size: 20px; |
||||
line-height: 26px; |
||||
text-transform: uppercase; |
||||
|
||||
@media ${devices.mobile} { |
||||
font-size: 14px; |
||||
line-height: 14px; |
||||
color: rgba(255, 255, 255, 0.5); |
||||
text-transform: none; |
||||
} |
||||
` |
||||
@ -0,0 +1,24 @@ |
||||
import type { Match } from 'features/Matches/hooks' |
||||
|
||||
export type MatchData = Pick<Match, ( |
||||
'id' |
||||
| 'team1' |
||||
| 'team2' |
||||
| 'sportType' |
||||
)> | null |
||||
|
||||
export enum PopupPages { |
||||
PLAYLIST, |
||||
SETTINGS, |
||||
} |
||||
|
||||
export enum PlayerPlaylistFormats { |
||||
ALL_MATCH_TIME = 1, |
||||
ALL_ACTIONS = 2, |
||||
SELECTED_ACTIONS = 3, |
||||
} |
||||
|
||||
export enum Teams { |
||||
TEAM1, |
||||
TEAM2, |
||||
} |
||||
@ -0,0 +1,24 @@ |
||||
import pipe from 'lodash/fp/pipe' |
||||
import orderBy from 'lodash/fp/orderBy' |
||||
import sumBy from 'lodash/fp/sumBy' |
||||
import uniqBy from 'lodash/fp/uniqBy' |
||||
|
||||
import type { Videos, Video } from './getVideos' |
||||
import { getVideos } from './getVideos' |
||||
|
||||
const calculateDuration = (videos: Videos) => { |
||||
const durationMs = pipe( |
||||
orderBy(({ quality }: Video) => Number(quality), 'desc'), |
||||
uniqBy(({ period }: Video) => period), |
||||
sumBy(({ duration }: Video) => duration), |
||||
)(videos) |
||||
return durationMs / 1000 |
||||
} |
||||
|
||||
/** |
||||
* Временный способ получения длительности матча |
||||
*/ |
||||
export const getFullMatchDuration = async (...args: Parameters<typeof getVideos>) => { |
||||
const videos = await getVideos(...args) |
||||
return calculateDuration(videos) |
||||
} |
||||
@ -0,0 +1,85 @@ |
||||
import { |
||||
DATA_URL, |
||||
PROCEDURES, |
||||
SportTypes, |
||||
} from 'config' |
||||
import { callApi, getSportLexic } from 'helpers' |
||||
|
||||
import { getFullMatchDuration } from './getFullMatchDuration' |
||||
|
||||
const proc = PROCEDURES.ott_match_popup |
||||
|
||||
type Args = { |
||||
matchId: number, |
||||
selectedActions: Array<number>, |
||||
sportType: SportTypes, |
||||
} |
||||
|
||||
type PlaylistData = { |
||||
/** episode end */ |
||||
e: number, |
||||
|
||||
/** match half/time */ |
||||
h: number, |
||||
|
||||
/** episode start */ |
||||
s: number, |
||||
} |
||||
|
||||
type Playlist = { |
||||
data: Array<PlaylistData>, |
||||
dur: number, |
||||
} |
||||
|
||||
type Player = { |
||||
id: number, |
||||
name_eng: string, |
||||
name_rus: string, |
||||
num: string, |
||||
} |
||||
|
||||
export type Players = Array<Player> |
||||
|
||||
export type MatchPlaylists = { |
||||
ball_in_play: Playlist, |
||||
fullMatchDuration: number, |
||||
goals: Playlist, |
||||
highlights: Playlist, |
||||
players1: Players, |
||||
players2: Players, |
||||
} |
||||
|
||||
type Response = { |
||||
data?: MatchPlaylists, |
||||
} |
||||
|
||||
export const getMatchPlaylists = async ({ |
||||
matchId, |
||||
selectedActions, |
||||
sportType, |
||||
}: Args) => { |
||||
const config = { |
||||
body: { |
||||
params: { |
||||
_p_actions: selectedActions, |
||||
_p_match_id: matchId, |
||||
}, |
||||
proc, |
||||
}, |
||||
} |
||||
|
||||
const playlistPromise: Promise<Response> = callApi({ |
||||
config, |
||||
url: `${DATA_URL}/${getSportLexic(sportType)}`, |
||||
}) |
||||
|
||||
const matchDurationPromise = getFullMatchDuration(sportType, matchId) |
||||
|
||||
const [playlist, fullMatchDuration] = await Promise.all( |
||||
[playlistPromise, matchDurationPromise], |
||||
) |
||||
|
||||
return playlist.data |
||||
? { ...playlist.data, fullMatchDuration } |
||||
: null |
||||
} |
||||
@ -0,0 +1,35 @@ |
||||
import { |
||||
DATA_URL, |
||||
PROCEDURES, |
||||
SportTypes, |
||||
} from 'config' |
||||
import { callApi, getSportLexic } from 'helpers' |
||||
|
||||
const proc = PROCEDURES.ott_match_popup_actions |
||||
|
||||
type Action = { |
||||
id: number, |
||||
lexic: number, |
||||
} |
||||
|
||||
export type Actions = Array<Action> |
||||
|
||||
type Response = { |
||||
data: Actions, |
||||
} |
||||
|
||||
export const getSportActions = async (sportType: SportTypes) => { |
||||
const config = { |
||||
body: { |
||||
params: {}, |
||||
proc, |
||||
}, |
||||
} |
||||
|
||||
const response: Response = await callApi({ |
||||
config, |
||||
url: `${DATA_URL}/${getSportLexic(sportType)}`, |
||||
}) |
||||
|
||||
return response.data |
||||
} |
||||