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
Mirlan 5 years ago committed by GitHub
parent f15ab39e79
commit c266ad2d60
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      public/images/back-icon.svg
  2. 24
      public/images/checkboxChecked.svg
  3. 17
      public/images/checkboxUnchecked.svg
  4. BIN
      public/images/preview2.png
  5. 34
      public/images/radioChecked.svg
  6. 17
      public/images/radioUnchecked.svg
  7. 11
      public/images/settings.svg
  8. 21
      src/config/lexics/indexLexics.tsx
  9. 2
      src/config/procedures.tsx
  10. 54
      src/features/App/AuthenticatedApp.tsx
  11. 4
      src/features/Common/Button/styled.tsx
  12. 53
      src/features/Common/Checkbox/Icon.tsx
  13. 31
      src/features/Common/Checkbox/index.tsx
  14. 57
      src/features/Common/Checkbox/styled.tsx
  15. 18
      src/features/Common/CloseButton/index.tsx
  16. 39
      src/features/Common/Radio/Icon.tsx
  17. 32
      src/features/Common/Radio/index.tsx
  18. 80
      src/features/Common/Radio/styled.tsx
  19. 1
      src/features/Common/index.tsx
  20. 2
      src/features/HomePage/index.tsx
  21. 14
      src/features/Icons/Close/index.tsx
  22. 4
      src/features/LexicsStore/hooks/useLexicsConfig.tsx
  23. 4
      src/features/LexicsStore/index.tsx
  24. 2
      src/features/LexicsStore/types.tsx
  25. 58
      src/features/MatchCard/CardBackside/index.tsx
  26. 28
      src/features/MatchCard/hooks.tsx
  27. 16
      src/features/MatchCard/index.tsx
  28. 60
      src/features/MatchCard/styled.tsx
  29. 14
      src/features/MatchPage/hooks/useVideoData.tsx
  30. 28
      src/features/MatchPopup/components/ApplyButton/index.tsx
  31. 16
      src/features/MatchPopup/components/BackButton/index.tsx
  32. 13
      src/features/MatchPopup/components/CloseButton/index.tsx
  33. 49
      src/features/MatchPopup/components/EpisodeDurationInputs/hooks.tsx
  34. 44
      src/features/MatchPopup/components/EpisodeDurationInputs/index.tsx
  35. 55
      src/features/MatchPopup/components/EpisodeDurationInputs/styled.tsx
  36. 73
      src/features/MatchPopup/components/InterviewCard/index.tsx
  37. 84
      src/features/MatchPopup/components/Interviews/index.tsx
  38. 80
      src/features/MatchPopup/components/MatchPlaylist/index.tsx
  39. 56
      src/features/MatchPopup/components/PlayerActions/index.tsx
  40. 102
      src/features/MatchPopup/components/PlayerActions/styled.tsx
  41. 46
      src/features/MatchPopup/components/PlayersList/index.tsx
  42. 90
      src/features/MatchPopup/components/PlayersList/styled.tsx
  43. 49
      src/features/MatchPopup/components/PlayersListDesktop/index.tsx
  44. 52
      src/features/MatchPopup/components/PlayersListMobile/index.tsx
  45. 47
      src/features/MatchPopup/components/PlayersListMobile/styled.tsx
  46. 87
      src/features/MatchPopup/components/PlaylistButton/index.tsx
  47. 52
      src/features/MatchPopup/components/PlaylistFormats/index.tsx
  48. 69
      src/features/MatchPopup/components/PlaylistFormats/styled.tsx
  49. 69
      src/features/MatchPopup/components/PlaylistPage/index.tsx
  50. 16
      src/features/MatchPopup/components/SettingsButton/index.tsx
  51. 39
      src/features/MatchPopup/components/SettingsDesktop/index.tsx
  52. 38
      src/features/MatchPopup/components/SettingsMobile/hooks.tsx
  53. 41
      src/features/MatchPopup/components/SettingsMobile/index.tsx
  54. 60
      src/features/MatchPopup/components/SettingsPage/index.tsx
  55. 4
      src/features/MatchPopup/config.tsx
  56. 49
      src/features/MatchPopup/index.tsx
  57. 102
      src/features/MatchPopup/store/hooks/index.tsx
  58. 44
      src/features/MatchPopup/store/hooks/usePopupNavigation.tsx
  59. 92
      src/features/MatchPopup/store/hooks/useSettingsState.tsx
  60. 26
      src/features/MatchPopup/store/hooks/useSportActions.tsx
  61. 20
      src/features/MatchPopup/store/index.tsx
  62. 140
      src/features/MatchPopup/styled.tsx
  63. 24
      src/features/MatchPopup/types.tsx
  64. 2
      src/features/Matches/helpers/prepareMatches.tsx
  65. 30
      src/features/Modal/index.tsx
  66. 22
      src/features/Modal/styled.tsx
  67. 2
      src/features/PlayerPage/index.tsx
  68. 5
      src/features/Register/components/AdditionalSubscription/index.tsx
  69. 5
      src/features/Register/components/MainSubscription/index.tsx
  70. 2
      src/features/TeamPage/index.tsx
  71. 2
      src/features/TournamentPage/index.tsx
  72. 17
      src/hooks/useStorage/index.tsx
  73. 24
      src/requests/getFullMatchDuration.tsx
  74. 85
      src/requests/getMatchPlaylists.tsx
  75. 35
      src/requests/getSportActions.tsx
  76. 12
      src/requests/getVideos.tsx
  77. 2
      src/requests/index.tsx

@ -0,0 +1,3 @@
<svg width="12" height="20" viewBox="0 0 12 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0.560678 8.93949L9.00002 0.500151C9.5523 -0.0521337 10.4477 -0.0521337 11 0.500151C11.5523 1.05244 11.5523 1.94787 11 2.50015L3.50002 10.0002L11 17.5002C11.5523 18.0524 11.5523 18.9479 11 19.5002C10.4477 20.0524 9.5523 20.0524 9.00002 19.5002L0.560678 11.0608C-0.0251087 10.475 -0.0251087 9.52528 0.560678 8.93949Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 444 B

@ -1,24 +0,0 @@
<svg width="26" height="27" viewBox="0 0 26 27" fill="none"
xmlns="http://www.w3.org/2000/svg">
<g filter="url(#filter0_d)">
<rect x="2" y="2" width="22" height="22" rx="2" fill="#005EDD"/>
<rect x="2" y="2" width="22" height="22" rx="2" fill="url(#paint0_linear)"/>
</g>
<path d="M13.1197 17.7526L11.4442 19.4281C11.109 19.7633 10.5825 19.7633 10.2473 19.4281L4.23937 13.4441C3.90426 13.109 3.90426 12.5824 4.23937 12.2473L5.9149 10.5718C6.25001 10.2367 6.7766 10.2367 7.11171 10.5718L13.1197 16.5797C13.4548 16.8909 13.4548 17.4414 13.1197 17.7526Z" fill="#222222"/>
<path d="M20.0849 6.23936L21.7605 7.91489C22.0956 8.25 22.0956 8.77659 21.7605 9.1117L11.444 19.4521C11.1089 19.7872 10.5823 19.7872 10.2472 19.4521L8.57164 17.7766C8.23654 17.4415 8.23654 16.9149 8.57164 16.5798L18.8881 6.23936C19.2232 5.92819 19.7498 5.92819 20.0849 6.23936Z" fill="#222222"/>
<defs>
<filter id="filter0_d" x="0" y="0.5" width="26" height="26" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
<feOffset dy="0.5"/>
<feGaussianBlur stdDeviation="1"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.35 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow" result="shape"/>
</filter>
<linearGradient id="paint0_linear" x1="13" y1="2" x2="13" y2="24" gradientUnits="userSpaceOnUse">
<stop stop-color="#005EDD"/>
<stop offset="1" stop-color="#0033CC"/>
</linearGradient>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

@ -1,17 +0,0 @@
<svg width="26" height="27" viewBox="0 0 26 27" fill="none"
xmlns="http://www.w3.org/2000/svg">
<g filter="url(#filter0_d)">
<rect x="3" y="3" width="20" height="20" rx="1" stroke="#005EDD" stroke-width="2"/>
</g>
<defs>
<filter id="filter0_d" x="0" y="0.5" width="26" height="26" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
<feOffset dy="0.5"/>
<feGaussianBlur stdDeviation="1"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.35 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow" result="shape"/>
</filter>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 860 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 211 KiB

@ -1,34 +0,0 @@
<svg width="28" height="28" viewBox="0 0 28 28" fill="none"
xmlns="http://www.w3.org/2000/svg">
<g filter="url(#filter0_d)">
<circle cx="14" cy="13.0001" r="12" fill="#005EDD"/>
<circle cx="14" cy="13.0001" r="12" fill="url(#paint0_radial)" fill-opacity="0.88"/>
</g>
<g filter="url(#filter1_d)">
<circle cx="14" cy="13.0001" r="4" fill="#0F0F0F"/>
</g>
<defs>
<filter id="filter0_d" x="0" y="0.00012207" width="28" height="28" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
<feOffset dy="1"/>
<feGaussianBlur stdDeviation="1"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.35 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow" result="shape"/>
</filter>
<filter id="filter1_d" x="7" y="6.00012" width="14" height="14" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
<feOffset/>
<feGaussianBlur stdDeviation="1.5"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow" result="shape"/>
</filter>
<radialGradient id="paint0_radial" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(14 13.0001) rotate(90) scale(21)">
<stop offset="0.427083" stop-color="#005EDD"/>
<stop offset="1"/>
</radialGradient>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

@ -1,17 +0,0 @@
<svg width="28" height="28" viewBox="0 0 28 28" fill="none"
xmlns="http://www.w3.org/2000/svg">
<g filter="url(#filter0_d)">
<circle cx="14" cy="13.0001" r="11" stroke="#005EDD" stroke-width="2"/>
</g>
<defs>
<filter id="filter0_d" x="0" y="0.00012207" width="28" height="28" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
<feOffset dy="1"/>
<feGaussianBlur stdDeviation="1"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.35 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow" result="shape"/>
</filter>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 853 B

@ -0,0 +1,11 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0)">
<path d="M12.7531 24H11.2469C10.0286 24 9.03745 23.0089 9.03745 21.7906V21.2811C8.51953 21.1156 8.01633 20.9067 7.53291 20.6565L7.17178 21.0177C6.29714 21.8934 4.89609 21.8677 4.04686 21.0173L2.98228 19.9528C2.13155 19.103 2.10708 17.7024 2.98256 16.8279L3.34341 16.467C3.09323 15.9836 2.88441 15.4805 2.71889 14.9625H2.20936C0.991172 14.9625 0 13.9714 0 12.7531V11.2469C0 10.0286 0.991172 9.0375 2.20941 9.0375H2.71894C2.88445 8.51953 3.09328 8.01638 3.34345 7.53295L2.98233 7.17188C2.10736 6.29784 2.1315 4.89712 2.98261 4.04695L4.04728 2.98233C4.89848 2.12995 6.2992 2.10867 7.17216 2.98261L7.53295 3.34341C8.01638 3.09328 8.51958 2.88441 9.0375 2.71889V2.20936C9.0375 0.991125 10.0286 0 11.2469 0H12.7531C13.9714 0 14.9625 0.991125 14.9625 2.20936V2.71894C15.4804 2.88441 15.9836 3.09328 16.467 3.34345L16.8282 2.98233C17.7028 2.10661 19.1039 2.1323 19.9531 2.98266L21.0177 4.04719C21.8684 4.89698 21.8929 6.29756 21.0174 7.17211L20.6565 7.53295C20.9067 8.01638 21.1155 8.51948 21.2811 9.0375H21.7906C23.0088 9.0375 24 10.0286 24 11.2469V12.7531C24 13.9714 23.0088 14.9625 21.7906 14.9625H21.2811C21.1155 15.4805 20.9067 15.9836 20.6565 16.467L21.0177 16.8282C21.8926 17.7022 21.8685 19.1029 21.0174 19.9531L19.9527 21.0177C19.1015 21.8701 17.7008 21.8914 16.8278 21.0174L16.467 20.6566C15.9836 20.9068 15.4804 21.1156 14.9625 21.2812V21.7907C14.9625 23.0089 13.9714 24 12.7531 24ZM7.76798 19.1798C8.43956 19.577 9.16238 19.8771 9.91631 20.0716C10.2268 20.1518 10.4438 20.4318 10.4438 20.7525V21.7906C10.4438 22.2335 10.8041 22.5938 11.2469 22.5938H12.7531C13.196 22.5938 13.5563 22.2335 13.5563 21.7906V20.7525C13.5563 20.4318 13.7732 20.1518 14.0837 20.0716C14.8377 19.8771 15.5605 19.577 16.2321 19.1798C16.5084 19.0164 16.8602 19.0609 17.0872 19.2879L17.8226 20.0233C18.1396 20.3407 18.6488 20.3334 18.9581 20.0236L20.0234 18.9584C20.3319 18.6502 20.3422 18.141 20.0237 17.8228L19.288 17.0871C19.061 16.8601 19.0166 16.5083 19.1799 16.232C19.5771 15.5605 19.8771 14.8377 20.0717 14.0837C20.1518 13.7732 20.4319 13.5563 20.7525 13.5563H21.7906C22.2335 13.5563 22.5938 13.196 22.5938 12.7532V11.2469C22.5938 10.8041 22.2335 10.4438 21.7906 10.4438H20.7525C20.4318 10.4438 20.1518 10.2269 20.0717 9.91641C19.8771 9.16242 19.5771 8.43961 19.1799 7.76808C19.0166 7.4918 19.061 7.13995 19.288 6.91298L20.0234 6.17756C20.3413 5.86003 20.333 5.35097 20.0237 5.04202L18.9585 3.97678C18.6497 3.66759 18.1404 3.65855 17.8229 3.9765L17.0872 4.7122C16.8603 4.93922 16.5083 4.98366 16.2321 4.82025C15.5605 4.42308 14.8377 4.12303 14.0838 3.92845C13.7733 3.84834 13.5563 3.56831 13.5563 3.24764V2.20936C13.5563 1.76653 13.196 1.40625 12.7532 1.40625H11.247C10.8041 1.40625 10.4438 1.76653 10.4438 2.20936V3.24755C10.4438 3.56822 10.2269 3.84825 9.91636 3.92836C9.16242 4.12294 8.43961 4.42298 7.76803 4.82016C7.49166 4.98352 7.13986 4.93908 6.91289 4.71211L6.17752 3.97669C5.86045 3.65925 5.35125 3.66661 5.04202 3.97636L3.97669 5.04164C3.66816 5.3498 3.65784 5.859 3.97641 6.17719L4.71211 6.91289C4.93908 7.13986 4.98352 7.4917 4.82016 7.76798C4.42298 8.43952 4.12298 9.16233 3.92841 9.91631C3.84825 10.2268 3.56822 10.4437 3.24759 10.4437H2.20941C1.76658 10.4437 1.40625 10.804 1.40625 11.2469V12.7531C1.40625 13.196 1.76658 13.5563 2.20941 13.5563H3.24755C3.56822 13.5563 3.8482 13.7732 3.92836 14.0836C4.12294 14.8376 4.42298 15.5604 4.82011 16.232C4.98347 16.5083 4.93903 16.8601 4.71206 17.0871L3.97664 17.8225C3.65873 18.14 3.66703 18.6491 3.97636 18.958L5.04159 20.0233C5.35036 20.3325 5.85961 20.3415 6.17714 20.0235L6.9128 19.2878C7.08005 19.1206 7.428 18.9788 7.76798 19.1798Z" fill="white"/>
<path d="M12.0002 17.2216C9.1208 17.2216 6.77832 14.8791 6.77832 11.9997C6.77832 9.12036 9.1208 6.77783 12.0002 6.77783C14.8796 6.77783 17.2221 9.12036 17.2221 11.9997C17.2221 14.8791 14.8796 17.2216 12.0002 17.2216ZM12.0002 8.18408C9.89621 8.18408 8.18457 9.89577 8.18457 11.9997C8.18457 14.1036 9.89626 15.8153 12.0002 15.8153C14.1041 15.8153 15.8158 14.1036 15.8158 11.9997C15.8158 9.89577 14.1042 8.18408 12.0002 8.18408Z" fill="white"/>
</g>
<defs>
<clipPath id="clip0">
<rect width="24" height="24" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 4.2 KiB

@ -1,5 +1,25 @@
import { proceduresLexics } from './procedures'
const matchPopupLexics = {
apply: 13491,
episode_duration: 13410,
go_back_to_match: 13405,
match_interviews: 13031,
match_playlist_ball_in_play: 2489,
match_playlist_full_game: 13028,
match_playlist_goals: 3559,
match_playlist_highlights: 13033,
match_settings: 13490,
playlist_format: 13406,
playlist_format_all_actions: 13408,
playlist_format_all_match_time: 13407,
playlist_format_selected_acions: 13409,
sec_after: 13412,
sec_before: 13411,
selected_player_actions: 13413,
team_players: 13398,
}
export const indexLexics = {
add_to_favorites: 1701,
add_to_favorites_error: 12943,
@ -49,4 +69,5 @@ export const indexLexics = {
watch_now: 13020,
...proceduresLexics,
...matchPopupLexics,
}

@ -20,6 +20,8 @@ export const PROCEDURES = {
get_user_subscriptions: 'get_user_subscriptions',
logout_user: 'logout_user',
lst_c_country: 'lst_c_country',
ott_match_popup: 'ott_match_popup',
ott_match_popup_actions: 'ott_match_popup_actions',
param_lexical: 'param_lexical',
save_user_custom_subscription: 'save_user_custom_subscription',
save_user_favorite: 'save_user_favorite',

@ -19,6 +19,7 @@ import { UserAccountForm } from 'features/UserAccount'
import { MatchSwitchesStore } from 'features/MatchSwitches'
import { UserFavoritesStore } from 'features/UserFavorites/store'
import { MatchPopupStore } from 'features/MatchPopup'
export const AuthenticatedApp = () => {
useLexicsConfig(indexLexics)
@ -27,31 +28,34 @@ export const AuthenticatedApp = () => {
<MatchSwitchesStore>
<UserFavoritesStore>
<ExtendedSearchStore>
{/* в Switch как прямой children можно рендерить только Route или Redirect */}
<Switch>
<Route path={PAGES.useraccount}>
<UserAccountForm />
</Route>
<Route exact path={PAGES.home}>
<HomePage />
</Route>
<Route path={`/:sportName${PAGES.tournament}/:pageId`}>
<TournamentPage />
</Route>
<Route path={`/:sportName${PAGES.team}/:pageId`}>
<TeamPage />
</Route>
<Route path={`/:sportName${PAGES.player}/:pageId`}>
<PlayerPage />
</Route>
<Route path={`/:sportName${PAGES.match}/:pageId`}>
<MatchPage />
</Route>
<Route path={PAGES.extendedSearch}>
<ExtendedSearchPage />
</Route>
<Redirect to={PAGES.home} />
</Switch>
<MatchPopupStore>
{/* в Switch как прямой children можно рендерить только Route или Redirect */}
<Switch>
<Route path={PAGES.useraccount}>
<UserAccountForm />
</Route>
<Route exact path={PAGES.home}>
<HomePage />
</Route>
<Route path={`/:sportName${PAGES.tournament}/:pageId`}>
<TournamentPage />
</Route>
<Route path={`/:sportName${PAGES.team}/:pageId`}>
<TeamPage />
</Route>
<Route path={`/:sportName${PAGES.player}/:pageId`}>
<PlayerPage />
</Route>
<Route path={`/:sportName${PAGES.match}/:pageId`}>
<MatchPage />
</Route>
<Route path={PAGES.extendedSearch}>
<ExtendedSearchPage />
</Route>
<Redirect to={PAGES.home} />
</Switch>
</MatchPopupStore>
</ExtendedSearchStore>
</UserFavoritesStore>
</MatchSwitchesStore>

@ -35,6 +35,10 @@ export const solidButtonStyles = css`
border-color: transparent;
background: ${({ theme: { colors } }) => colors.primary};
/* TODO: удалить медиа запросы из стайледа,
добавить специфичные юз кейсу правила
через враппер компоненты или др способом
*/
@media ${devices.mobile} {
width: 335px;
height: 40px;

@ -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,5 +1,9 @@
import { InputHTMLAttributes } from 'react'
import type { LexicsId } from 'features/LexicsStore/types'
import { T9n } from 'features/T9n'
import { Icon } from './Icon'
import {
Wrapper,
Input,
@ -8,28 +12,37 @@ import {
type Props = Pick<InputHTMLAttributes<HTMLInputElement>, (
| 'checked'
| 'className'
| 'id'
| 'name'
| 'onClick'
| 'onChange'
)> & {
label?: string,
labelLexic?: LexicsId,
}
export const Checkbox = ({
checked,
className,
id,
label,
labelLexic,
name,
onChange,
onClick,
}: Props) => (
<Wrapper>
<Input
id={id}
type='checkbox'
name={name}
checked={checked}
onClick={onClick}
/>
<Label htmlFor={id}>{label}</Label>
<Wrapper className={className}>
<Label>
<Input
id={id}
name={name}
checked={checked}
onClick={onClick}
onChange={onChange}
/>
<Icon checked={checked} />
{labelLexic ? <T9n t={labelLexic} /> : label}
</Label>
</Wrapper>
)

@ -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,8 +1,8 @@
import { InputHTMLAttributes } from 'react'
import { useRouteMatch } from 'react-router-dom'
import { PAGES } from 'config'
import { T9n } from 'features/T9n'
import { Icon } from './Icon'
import {
Wrapper,
Input,
@ -11,35 +11,37 @@ import {
type Props = Pick<InputHTMLAttributes<HTMLInputElement>, (
| 'checked'
| 'className'
| 'id'
| 'name'
| 'onChange'
| 'onClick'
)> & {
label?: string,
labelLexic?: string,
}
export const Radio = ({
checked,
className,
id,
label = '',
labelLexic,
name,
onChange,
onClick,
}: Props) => {
const isUserAccountPage = useRouteMatch(PAGES.useraccount)?.isExact || false
return (
<Wrapper isUserAccountPage={isUserAccountPage}>
}: Props) => (
<Wrapper className={className}>
<Label>
<Input
id={id}
type='radio'
name={name}
checked={checked}
onClick={onClick}
isUserAccountPage={isUserAccountPage}
onChange={onChange}
/>
<Label htmlFor={id}>
{label}
</Label>
</Wrapper>
)
}
<Icon checked={checked} />
{labelLexic ? <T9n t={labelLexic} /> : label}
</Label>
</Wrapper>
)

@ -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;
}
`
: '')}
}
`

@ -3,7 +3,6 @@ export * from './Button'
export * from './Radio'
export * from './Checkbox'
export * from './Arrows'
export * from './CloseButton'
export * from './SportName'
export * from './StarIcon'
export * from './customScrollbar'

@ -15,6 +15,7 @@ import {
import { SportFilterWrapper } from 'features/ProfileHeader/styled'
import { MainWrapper } from 'features/MainWrapper'
import { UserFavorites } from 'features/UserFavorites'
import { MatchPopup } from 'features/MatchPopup'
import { useHomePage } from './hooks'
import { Header } from './components/Header'
@ -24,6 +25,7 @@ const Home = () => {
const { fetchMatches } = useHomePage()
return (
<MainWrapper>
<MatchPopup />
<MediaQuery maxDevice='tablet'>
<HeaderMobile />
<HeaderMobileMidle>

@ -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,5 +1,7 @@
import { useState, useCallback } from 'react'
import isEmpty from 'lodash/isEmpty'
import { getObjectConfig } from 'features/LexicsStore/helpers'
import type { LexicsConfig, LexicsId } from '../types'
@ -9,6 +11,8 @@ export const useLexicsConfig = () => {
const addLexicsConfig = useCallback(
(ids: Array<LexicsId> | LexicsConfig) => {
if (isEmpty(ids)) return
const config = getObjectConfig(ids)
setLexicsConfig((state) => ({ ...state, ...config }))
},

@ -5,7 +5,7 @@ import {
useEffect,
} from 'react'
import { LexicsConfig } from './types'
import type { LexicsConfig, LexicsId } from './types'
import { useLexics } from './hooks'
type Context = ReturnType<typeof useLexics>
@ -20,7 +20,7 @@ export const LexicsStore = ({ children }: Props) => {
export const useLexicsStore = () => useContext(LexicsContext)
export const useLexicsConfig = (config: LexicsConfig) => {
export const useLexicsConfig = (config: Array<LexicsId> | LexicsConfig) => {
const { addLexicsConfig } = useLexicsStore()
useEffect(() => {

@ -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,
}
}

@ -5,7 +5,6 @@ import { PAGES } from 'config'
import type { Match } from 'features/Matches'
import { CardFrontside } from './CardFrontside'
import { CardBackside } from './CardBackside'
import { useCard } from './hooks'
type Props = {
@ -15,26 +14,15 @@ type Props = {
export const MatchCard = ({ match }: Props) => {
const isHomePage = useRouteMatch(PAGES.home)?.isExact
const {
close,
flipCard,
isOpen,
onKeyPress,
openMatchPopup,
} = useCard(match)
if (isOpen) {
return (
<CardBackside
match={match}
onClose={close}
/>
)
}
return (
<CardFrontside
match={match}
showSportName={isHomePage}
onClick={flipCard}
onClick={openMatchPopup}
onKeyPress={onKeyPress}
/>
)

@ -2,7 +2,6 @@ import styled from 'styled-components/macro'
import { devices } from 'config/devices'
import { T9n } from 'features/T9n'
import { Name } from 'features/Name'
import { ProfileLogo } from 'features/ProfileLogo'
@ -142,65 +141,6 @@ export const Score = styled.div`
width: 10%;
`
export const Rows = styled.div`
width: fit-content;
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: pointer;
height: 288px;
@media ${devices.laptop} {
height: 279px;
}
@media ${devices.tablet} {
height: 299px;
}
`
export const CardHoverInner = styled.div`
position: relative;
overflow: hidden;
width: fit-content;
`
export const CardHoverTitle = styled(T9n)`
font-size: 10px;
letter-spacing: 0.03em;
text-transform: uppercase;
color: rgba(255, 255, 255, 0.5);
`
export const TeamLogos = styled.div`
display: flex;
padding-left: 24px;

@ -4,9 +4,6 @@ import {
useState,
} from 'react'
import isEmpty from 'lodash/isEmpty'
import filter from 'lodash/filter'
import type { LiveVideos, Videos } from 'requests'
import { getLiveVideos, getVideos } from 'requests'
@ -14,21 +11,14 @@ import { useSportNameParam, usePageId } from 'hooks'
import { useLastPlayPosition } from './useLastPlayPosition'
const filterByIds = (videos: Videos) => {
const zeroIdVideos = filter(videos, { abc: '0' })
return isEmpty(zeroIdVideos) ? videos : zeroIdVideos
}
export const useVideoData = () => {
const [videos, setVideos] = useState<Videos>([])
const [liveVideos, setLiveVideos] = useState<LiveVideos>([])
const { sportType } = useSportNameParam()
const matchId = usePageId()
const fetchMatchVideos = useCallback(async () => {
const videosResponse = await getVideos(sportType, matchId)
const filteredVideosResponseByAbc = filterByIds(videosResponse)
setVideos(filteredVideosResponseByAbc)
const fetchMatchVideos = useCallback(() => {
getVideos(sportType, matchId).then(setVideos)
}, [sportType, matchId])
useEffect(() => {

@ -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,
}

@ -10,7 +10,6 @@ const prepareMatch = ({
date,
has_video,
id,
live,
preview,
sport,
storage,
@ -26,7 +25,6 @@ const prepareMatch = ({
id,
isClickable: (sub && access && (
has_video
|| live
|| storage
)),
preview,

@ -1,8 +1,9 @@
import type { ReactNode } from 'react'
import { useRef } from 'react'
import { useMemo } from 'react'
import ReactDOM from 'react-dom'
import { OutsideClick } from 'features/OutsideClick'
import { Close } from 'features/Icons/Close'
import {
ModalContainer,
@ -10,30 +11,43 @@ import {
ModalCloseButton,
} from './styled'
const defaultCloseHandler = () => {}
type Props = {
children: ReactNode,
close: () => void,
className?: string,
close?: () => void,
isOpen: boolean,
withCloseButton?: boolean,
}
export const Modal = ({
children,
close,
className,
close = defaultCloseHandler,
isOpen,
withCloseButton = true,
}: Props) => {
const modalRoot = useRef(document.getElementById('modal-root'))
const modalRoot = useMemo(
() => document.getElementById('modal-root'),
[],
)
return isOpen
return (isOpen && modalRoot)
? ReactDOM.createPortal(
<ModalContainer>
<ModalContainer className={className}>
<OutsideClick onClick={close}>
<ModalWindow>
<ModalCloseButton onClick={close} />
{withCloseButton && (
<ModalCloseButton onClick={close}>
<Close />
</ModalCloseButton>
)}
{children}
</ModalWindow>
</OutsideClick>
</ModalContainer>,
modalRoot.current as Element,
modalRoot,
)
: null
}

@ -2,8 +2,6 @@ import styled from 'styled-components/macro'
import { devices } from 'config/devices'
import { CloseButton } from 'features/Common/CloseButton'
export const ModalContainer = styled.div`
position: fixed;
top: 0;
@ -17,25 +15,31 @@ export const ModalContainer = styled.div`
color: white;
font-weight: 600;
`
export const ModalWindow = styled.div`
background-color: #313131;
position: relative;
padding: 15px;
box-shadow: 0px 5px 30px rgba(0, 0, 0, 0.7);
border-radius: 10px;
@media ${devices.mobile} {
height: 100vh;
width: 100vw;
border-radius: 0;
}
`
export const ModalCloseButton = styled(CloseButton)`
margin-right: 19px;
margin-top: 16px;
width: 16px;
height: 16px;
export const ModalCloseButton = styled.button.attrs({
'aria-label': 'Close',
})`
position: absolute;
top: 0;
right: 0;
border-style: none;
outline: none;
padding: 16px 19px;
cursor: pointer;
color: rgba(255, 255, 255, 0.5);
background-color: transparent;
`

@ -6,6 +6,7 @@ import { Matches } from 'features/Matches'
import { UserFavorites } from 'features/UserFavorites'
import { MainWrapper } from 'features/MainWrapper'
import { MediaQuery } from 'features/MediaQuery'
import { MatchPopup } from 'features/MatchPopup'
import { usePlayerPage } from './hooks'
import { Content } from './styled'
@ -19,6 +20,7 @@ export const PlayerPage = () => {
return (
<MainWrapper>
<MatchPopup />
<MediaQuery minDevice='laptop'>
<UserFavorites />
</MediaQuery>

@ -1,4 +1,5 @@
import { Checkbox } from 'features/Common'
import { MediaQuery } from 'features/MediaQuery'
import { Price } from '../Price'
import {
@ -15,7 +16,9 @@ type Props = {
export const AdditionalSubscription = ({ price, title }: Props) => (
<PriceItem>
<PriceItemCol>
<Checkbox name='additionalSubscription' id={title} />
<MediaQuery minDevice='laptop'>
<Checkbox name='additionalSubscription' id={title} />
</MediaQuery>
</PriceItemCol>
<PriceItemCol>
<PriceItemTitle>{title}</PriceItemTitle>

@ -1,4 +1,5 @@
import { Radio } from 'features/Common'
import { MediaQuery } from 'features/MediaQuery'
import { Price } from '../Price'
import {
@ -15,7 +16,9 @@ type Props = {
export const MainSubscription = ({ price, title }: Props) => (
<SubscriptionWrapper>
<Row>
<Radio name='mainSubscription' id={title} />
<MediaQuery minDevice='laptop'>
<Radio name='mainSubscription' id={title} />
</MediaQuery>
<SubscriptionTitle>{title}</SubscriptionTitle>
</Row>
<Row isPriceRow>

@ -6,6 +6,7 @@ import { Matches } from 'features/Matches'
import { UserFavorites } from 'features/UserFavorites'
import { MainWrapper } from 'features/MainWrapper'
import { MediaQuery } from 'features/MediaQuery'
import { MatchPopup } from 'features/MatchPopup'
import { useTeamPage } from './hooks'
import { Content } from './styled'
@ -19,6 +20,7 @@ export const TeamPage = () => {
return (
<MainWrapper>
<MatchPopup />
<MediaQuery minDevice='laptop'>
<UserFavorites />
</MediaQuery>

@ -6,6 +6,7 @@ import { Matches } from 'features/Matches'
import { MainWrapper } from 'features/MainWrapper'
import { UserFavorites } from 'features/UserFavorites'
import { MediaQuery } from 'features/MediaQuery'
import { MatchPopup } from 'features/MatchPopup'
import { useTournamentPage } from './hooks'
import { Content } from './styled'
@ -19,6 +20,7 @@ export const TournamentPage = () => {
return (
<MainWrapper>
<MatchPopup />
<MediaQuery minDevice='laptop'>
<UserFavorites />
</MediaQuery>

@ -1,8 +1,4 @@
import {
useState,
useCallback,
useEffect,
} from 'react'
import { useState, useEffect } from 'react'
import { queryParamStorage } from 'features/QueryParamsStorage'
@ -46,11 +42,6 @@ const createHook = (storage: Storage) => (
const [state, setState] = useState<T>(getInitialState)
const setStateAndSave = useCallback((value: T) => {
storage.setItem(key, JSON.stringify(value, dateReplacer))
setState(value)
}, [key])
useEffect(() => {
const storeValue = readStorageInitialValue(storage, key)
const isValid = validator(storeValue)
@ -59,7 +50,11 @@ const createHook = (storage: Storage) => (
}
}, [key, validator])
return [state, setStateAndSave] as const
useEffect(() => {
storage.setItem(key, JSON.stringify(state, dateReplacer))
}, [key, state])
return [state, setState] as const
}
)

@ -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
}

@ -1,7 +1,15 @@
import isEmpty from 'lodash/isEmpty'
import filter from 'lodash/filter'
import { API_ROOT, SportTypes } from 'config'
import { callApi } from 'helpers'
type Video = {
const filterByIds = (videos: Videos) => {
const zeroIdVideos = filter(videos, { abc: '0' })
return isEmpty(zeroIdVideos) ? videos : zeroIdVideos
}
export type Video = {
/** id дорожки */
abc: string,
duration: number,
@ -27,5 +35,5 @@ export const getVideos = (
return callApi({
config,
url: `${API_ROOT}/videoapi`,
})
}).then(filterByIds)
}

@ -21,3 +21,5 @@ export * from './getPlayerInfo'
export * from './getLiveVideos'
export * from './getMatchLastWatchSeconds'
export * from './getMatchesPreviewImages'
export * from './getSportActions'
export * from './getMatchPlaylists'

Loading…
Cancel
Save