Compare commits

..

1 Commits

Author SHA1 Message Date
Rakov 66a002e834 fix(#713): rustat client 2 years ago
  1. 53
      .drone.yml
  2. 9
      Makefile
  3. BIN
      public/clients/facr/favicon/android-chrome-192x192.png
  4. BIN
      public/clients/facr/favicon/android-chrome-512x512.png
  5. BIN
      public/clients/facr/favicon/apple-touch-icon.png
  6. BIN
      public/clients/facr/favicon/favicon-16x16.png
  7. BIN
      public/clients/facr/favicon/favicon-32x32.png
  8. BIN
      public/clients/facr/favicon/favicon.ico
  9. BIN
      public/clients/fqtv/favicon/android-chrome-192x192.png
  10. BIN
      public/clients/fqtv/favicon/android-chrome-512x512.png
  11. BIN
      public/clients/fqtv/favicon/apple-touch-icon.png
  12. BIN
      public/clients/fqtv/favicon/favicon-16x16.png
  13. BIN
      public/clients/fqtv/favicon/favicon-32x32.png
  14. BIN
      public/clients/fqtv/favicon/favicon.ico
  15. BIN
      public/clients/lff/favicon/android-chrome-192x192.png
  16. BIN
      public/clients/lff/favicon/android-chrome-512x512.png
  17. BIN
      public/clients/lff/favicon/apple-touch-icon.png
  18. BIN
      public/clients/lff/favicon/favicon-16x16.png
  19. BIN
      public/clients/lff/favicon/favicon-32x32.png
  20. BIN
      public/clients/lff/favicon/favicon.ico
  21. 16
      public/images/logo-rustat.svg
  22. 4
      public/images/matchTabs/likes.svg
  23. 15
      public/silent-refresh.html
  24. 3
      src/config/clients/index.tsx
  25. 53
      src/config/clients/rustat.tsx
  26. 4
      src/config/clients/types.tsx
  27. 18
      src/config/lexics/indexLexics.tsx
  28. 1
      src/config/payments.tsx
  29. 7
      src/config/routes.tsx
  30. 6
      src/features/AuthServiceApp/components/ConfirmPopup/index.tsx
  31. 2
      src/features/AuthServiceApp/config/clients/index.tsx
  32. 42
      src/features/AuthServiceApp/config/clients/rustat.tsx
  33. 2
      src/features/AuthServiceApp/config/lexics.tsx
  34. 3
      src/features/AuthStore/helpers.tsx
  35. 26
      src/features/AuthStore/hooks/useAuth.tsx
  36. 2
      src/features/BuyMatchPopup/types.tsx
  37. 14
      src/features/HeaderFilters/components/DateFilter/helpers.tsx
  38. 10
      src/features/HeaderFilters/components/DateFilter/hooks/index.tsx
  39. 143
      src/features/HeaderFilters/components/DateFilter/index.tsx
  40. 113
      src/features/HeaderFilters/components/DateFilter/styled.tsx
  41. 193
      src/features/HeaderFilters/components/FacrDateFilter/index.tsx
  42. 1
      src/features/HeaderFilters/index.tsx
  43. 1
      src/features/HeaderFilters/store/config.tsx
  44. 16
      src/features/HeaderFilters/store/hooks/index.tsx
  45. 5
      src/features/HeaderMobile/index.tsx
  46. 2
      src/features/HeaderMobile/styled.tsx
  47. 5
      src/features/HomePage/components/Header/index.tsx
  48. 3
      src/features/HomePage/components/HeaderFilters/index.tsx
  49. 3
      src/features/HomePage/components/HeaderFilters/styled.tsx
  50. 8
      src/features/HomePage/index.tsx
  51. 3
      src/features/Icon/index.tsx
  52. 2
      src/features/MatchCard/CardFrontside/MatchCardMobile/index.tsx
  53. 11
      src/features/MatchCard/CardFrontside/hooks.tsx
  54. 8
      src/features/MatchCard/CardFrontside/index.tsx
  55. 4
      src/features/MatchCard/config.tsx
  56. 2
      src/features/MatchCard/hooks.tsx
  57. 2
      src/features/MatchCard/index.tsx
  58. 58
      src/features/MatchPage/store/atoms.tsx
  59. 75
      src/features/MatchPage/store/hooks/useLikes.tsx
  60. 4
      src/features/MatchPage/store/hooks/useTournamentData.tsx
  61. 2
      src/features/MatchPage/store/index.tsx
  62. 3
      src/features/MatchPage/types.tsx
  63. 2
      src/features/MatchPopup/types.tsx
  64. 126
      src/features/MatchSidePlaylists/components/LikeEvent/index.tsx
  65. 35
      src/features/MatchSidePlaylists/components/LikeEvent/styled.tsx
  66. 45
      src/features/MatchSidePlaylists/components/LikesList/index.tsx
  67. 2
      src/features/MatchSidePlaylists/components/TabEvents/styled.tsx
  68. 235
      src/features/MatchSidePlaylists/components/TabLikes/index.tsx
  69. 40
      src/features/MatchSidePlaylists/components/TabLikes/styled.tsx
  70. 1
      src/features/MatchSidePlaylists/config.tsx
  71. 4
      src/features/MatchSidePlaylists/hooks.tsx
  72. 62
      src/features/MatchSidePlaylists/index.tsx
  73. 25
      src/features/MatchSidePlaylists/styled.tsx
  74. 2
      src/features/Matches/components/MatchesList/index.tsx
  75. 4
      src/features/Matches/helpers/addSportType.tsx
  76. 4
      src/features/Matches/helpers/getMatchClickAction/__tests__/index.tsx
  77. 4
      src/features/Matches/helpers/getMatchClickAction/index.tsx
  78. 19
      src/features/Matches/helpers/prepareMatches.tsx
  79. 4
      src/features/Matches/hooks.tsx
  80. 2
      src/features/Matches/index.tsx
  81. 7
      src/features/MatchesGrid/index.tsx
  82. 77
      src/features/MatchesSlider/hooks.tsx
  83. 98
      src/features/MatchesSlider/index.tsx
  84. 95
      src/features/MatchesSlider/styled.tsx
  85. 168
      src/features/MatchesTimeline/hooks.tsx
  86. 151
      src/features/MatchesTimeline/index.tsx
  87. 72
      src/features/MatchesTimeline/styled.tsx
  88. 5
      src/features/PageLayout/styled.tsx
  89. 2
      src/features/ProfileHeader/styled.tsx
  90. 1
      src/features/SportsFilter/components/SelectSport/styled.tsx
  91. 14
      src/features/StreamPlayer/components/Controls/Components/ControlsMobile/index.tsx
  92. 1
      src/features/StreamPlayer/components/Controls/Components/ControlsMobile/styled.tsx
  93. 13
      src/features/StreamPlayer/components/Controls/Components/ControlsWeb/index.tsx
  94. 3
      src/features/StreamPlayer/components/Controls/index.tsx
  95. 164
      src/features/StreamPlayer/components/LikeButton/index.tsx
  96. 78
      src/features/StreamPlayer/components/LikeButton/styled.tsx
  97. 4
      src/features/StreamPlayer/hooks/index.tsx
  98. 138
      src/features/StreamPlayer/hooks/useParticles.tsx
  99. 23
      src/features/StreamPlayer/index.tsx
  100. 8
      src/features/StreamPlayer/styled.tsx
  101. Some files were not shown because too many files have changed in this diff Show More

@ -1007,56 +1007,3 @@ steps:
- aws cloudfront create-invalidation --distribution-id E15IFY23VM147K --paths "/*" - aws cloudfront create-invalidation --distribution-id E15IFY23VM147K --paths "/*"
depends_on: depends_on:
- make-rustat - make-rustat
---
kind: pipeline
type: docker
name: deploy insport.live
concurrency:
limit: 1
platform:
os: linux
arch: amd64
trigger:
ref:
- refs/heads/insport.live
steps:
- name: npm-install
image: node:16-alpine
environment:
REACT_APP_STRIPE_PK:
from_secret: REACT_APP_STRIPE_PK
commands:
- apk add --no-cache make
- npm install --legacy-peer-deps
- name: make-insport-live
image: node:16-alpine
environment:
REACT_APP_STRIPE_PK:
from_secret: REACT_APP_STRIPE_PK
commands:
- apk add --no-cache make
- make insport-live-prod
depends_on:
- npm-install
- name: deploy-insport-live
image: amazon/aws-cli:latest
environment:
AWS_ACCESS_KEY_ID:
from_secret: AWS_ACCESS_KEY_ID
AWS_SECRET_ACCESS_KEY:
from_secret: AWS_SECRET_ACCESS_KEY
AWS_DEFAULT_REGION:
from_secret: AWS_DEFAULT_REGION
AWS_MAX_ATTEMPTS: 10
commands:
- aws s3 sync build_insport_live s3://insports-live --delete
- aws cloudfront create-invalidation --distribution-id E1LBC88VYP6XVB --paths "/*"
depends_on:
- make-insport-live

@ -225,15 +225,6 @@ rustat-prod:
BUILD_PATH=build_rustat \ BUILD_PATH=build_rustat \
npm run build && cp -r .well-known build_rustat npm run build && cp -r .well-known build_rustat
insport-live-prod:
rm -rf build_insport_live && \
REACT_APP_TYPE=ott \
REACT_APP_ENV=staging \
REACT_APP_CLIENT=lff \
REACT_APP_STRIPE_PK=pk_live_51J5TEYEDSxVnTgDW5XxhC6ntKZKddXgKHq5HOCDmJTdfSKluMYCdLHOcUA3Miuy8HesxG1eS4c0dQRQpMsEHRrQL00USpu5xIq \
BUILD_PATH=build_insport_live \
npm run build && cp -r .well-known build_insport_live
deploy-all: prod preprod facr-prod lff-prod diwansport-prod india-prod fqtv-prod rustat-prod deploy-all: prod preprod facr-prod lff-prod diwansport-prod india-prod fqtv-prod rustat-prod
test: test:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 793 B

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 802 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 93 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 884 B

After

Width:  |  Height:  |  Size: 598 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 102 KiB

@ -0,0 +1,16 @@
<svg width="154" height="40" viewBox="0 0 154 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_4_13767)">
<path d="M23.5999 23.48C25.4899 24.44 27.2599 25.57 28.8899 26.85C29.2799 31.37 28.6899 35.84 27.2699 40H10.0099C8.56987 40 7.29987 39.08 6.85987 37.71L6.14987 35.53C12.5699 32.89 17.9999 28.35 21.7399 22.61C22.3599 22.88 22.9799 23.17 23.5899 23.48H23.5999ZM31.6999 40C33.1399 40 34.4099 39.08 34.8499 37.71L35.8999 34.48C34.4099 32.21 32.6499 30.15 30.6299 28.33C30.7799 32.33 30.2099 36.28 29.0099 40H31.6999ZM18.9099 0.63L16.7399 2.21C19.8999 4.52 22.6899 7.38 24.9199 10.71C25.4099 8.36 25.6299 5.95 25.5899 3.49C25.5899 3.2 25.5699 2.91 25.5599 2.63L22.8099 0.63C21.6499 -0.21 20.0799 -0.21 18.9199 0.63H18.9099ZM16.5499 26.5C17.9099 25.09 19.1199 23.57 20.1899 21.98C16.5699 20.63 12.7799 19.94 8.95987 19.94C6.42987 19.94 3.87987 20.24 1.37987 20.85L5.63987 33.96C9.69987 32.28 13.4099 29.76 16.5499 26.5ZM25.4699 14.92C24.7299 17.11 23.7699 19.21 22.6199 21.18C23.1999 21.44 23.7799 21.71 24.3499 22C25.8399 22.76 27.2599 23.62 28.6099 24.57C28.3499 22.92 27.9599 21.26 27.4199 19.62C26.8899 17.98 26.2299 16.41 25.4699 14.92ZM26.1399 12.68C27.2899 14.68 28.2499 16.83 28.9899 19.11C29.7299 21.39 30.2199 23.7 30.4599 26C32.7699 27.89 34.8099 30.07 36.5399 32.48L41.5399 17.08C41.9799 15.72 41.4999 14.22 40.3399 13.38L27.2399 3.85C27.2599 6.9 26.8799 9.86 26.1399 12.68ZM15.3199 3.24L1.36987 13.38C0.209872 14.22 -0.280128 15.72 0.169872 17.08L0.879872 19.27C7.62987 17.61 14.6399 18.06 21.0999 20.55C22.5199 18.16 23.6199 15.61 24.3699 12.94C22.0299 9.05 18.9299 5.77 15.3299 3.23L15.3199 3.24Z" fill="white"/>
<path d="M66.5501 15.84C63.8001 15.94 61.7301 16.42 60.2801 16.94V24.15H64.1001V27.58H54.1401V24.15H56.5701V15.71H54.1401V12.28H60.2801V13.93C61.8001 12.83 63.9001 12.02 66.5501 12.02V15.84Z" fill="white"/>
<path d="M88.2201 27.58H82.0101V26.32C80.6801 27.42 79.1301 28.07 77.2901 28.07C73.2801 28.07 70.6001 25.16 70.6001 20.73V15.72H68.1401V12.29H74.3201V20.5C74.3201 23.15 75.7401 24.67 78.0701 24.67C79.4001 24.67 80.8501 23.89 82.0201 22.44V15.71H79.5901V12.28H85.7301V24.15H88.2201V27.58Z" fill="white"/>
<path d="M119.26 27.42C117.58 27.94 116.77 28.07 115.44 28.07C111.82 28.07 109.52 25.64 109.52 21.7V15.72H105.77V12.29H109.52V7.54004H113.24V12.29H118.64V15.72H113.24V21.7C113.24 23.74 114.34 24.61 116.18 24.61C117.02 24.61 117.96 24.45 118.74 24.22L119.26 27.42Z" fill="white"/>
<path d="M136.3 24.15H138.76V27.58H132.58V26.51C131.32 27.42 129.73 27.93 127.89 27.93C123.62 27.93 120.19 24.37 120.19 19.94C120.19 15.51 123.62 11.92 127.89 11.92C129.73 11.92 131.32 12.44 132.58 13.34V12.27H138.76V15.7H136.3V24.14V24.15ZM132.58 22.92V16.94C131.25 15.65 129.9 15.16 128.44 15.16C125.82 15.16 123.91 17.39 123.91 19.95C123.91 22.51 125.82 24.7 128.44 24.7C129.9 24.7 131.25 24.21 132.58 22.92Z" fill="white"/>
<path d="M153.82 27.42C152.14 27.94 151.33 28.07 150 28.07C146.38 28.07 144.08 25.64 144.08 21.7V15.72H140.33V12.29H144.08V7.54004H147.8V12.29H153.2V15.72H147.8V21.7C147.8 23.74 148.9 24.61 150.74 24.61C151.58 24.61 152.52 24.45 153.29 24.22L153.81 27.42H153.82Z" fill="white"/>
<path d="M99.7301 18.52L96.6601 18C94.8801 17.68 94.3301 17.03 94.3301 16.22C94.3301 15.41 95.0101 14.64 96.7201 14.64C98.6301 14.64 99.5701 15.55 100.83 16.94H104.19V12.28H100.83V13.22C99.8901 12.31 98.5301 11.8 96.6601 11.8C93.1001 11.8 90.4201 13.42 90.4201 16.49C90.4201 19.34 92.7501 20.73 96.0501 21.31L98.8301 21.76C100.22 22.02 100.8 22.67 100.8 23.51C100.8 24.35 100.25 25.16 98.2801 25.16C95.9401 25.16 94.6101 23.95 93.2701 22.35H89.8101V27.58H93.2101V26.19C94.4101 27.19 96.2201 28.07 98.7101 28.07C102.43 28.07 104.6 26.16 104.6 23.28C104.6 20.24 102.17 18.95 99.7501 18.53L99.7301 18.52Z" fill="white"/>
</g>
<defs>
<clipPath id="clip0_4_13767">
<rect width="153.82" height="40" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 3.8 KiB

@ -1,4 +0,0 @@
<svg width="25" height="25" viewBox="0 0 25 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.9261 5.8212C12.9307 5.87963 12.9358 5.93231 12.9406 5.97754C12.9332 5.99732 12.9251 6.019 12.9163 6.04251C12.8651 6.17851 12.7898 6.37523 12.6939 6.61585C12.502 7.09769 12.229 7.75251 11.9041 8.44659C11.5783 9.14247 11.2053 9.86744 10.8143 10.4938C10.4154 11.1329 10.0302 11.616 9.69372 11.8772C9.66718 11.8978 9.64228 11.9205 9.61923 11.9449C9.18111 12.41 8.91275 12.9173 8.74763 13.3195L8.7475 13.3194L8.74278 13.3316C8.71208 13.4109 8.6845 13.4866 8.66007 13.556C8.58397 13.7007 8.41439 13.9457 8.03684 14.1757C7.79824 14.321 7.67921 14.6026 7.74121 14.875L9.45857 22.4214C9.51892 22.6866 9.738 22.8862 10.0077 22.9216L10.0924 22.2772C10.0077 22.9216 10.0077 22.9216 10.0078 22.9216L10.008 22.9217L10.0085 22.9217L10.0104 22.922L10.0169 22.9228L10.0413 22.9259L10.1335 22.9373C10.2137 22.9471 10.3304 22.9608 10.4785 22.977C10.7745 23.0094 11.1963 23.052 11.7023 23.0927C12.704 23.1733 14.0447 23.2476 15.3961 23.2207C16.4734 23.3062 17.6651 23.3176 18.6492 23.1041C20.351 22.7378 21.1491 21.9031 21.424 21.0082C21.5524 20.5903 21.5506 20.2065 21.5195 19.9324C21.5184 19.923 21.5173 19.9137 21.5162 19.9045C22.2578 19.2016 22.4576 18.4095 22.4234 17.7496C22.407 17.4337 22.3384 17.1612 22.268 16.9564C22.532 16.603 22.6944 16.243 22.7698 15.8841C22.869 15.4117 22.8081 14.9852 22.6891 14.6367C22.5848 14.3315 22.4352 14.0826 22.3041 13.9021C22.3596 13.7543 22.414 13.5732 22.4473 13.3692C22.5426 12.7857 22.4608 11.9957 21.7482 11.3383C21.301 10.9249 20.7077 10.7201 20.1465 10.6173C19.5762 10.5129 18.9701 10.5014 18.4303 10.5271C17.8871 10.553 17.3897 10.6177 17.0293 10.6755C16.8485 10.7044 16.7006 10.7319 16.5969 10.7524C16.545 10.7626 16.504 10.7711 16.4753 10.7772L16.4465 10.7835C16.2767 10.8127 16.1004 10.8472 15.9166 10.8879C15.9176 10.8109 15.9236 10.7124 15.9386 10.5884C15.9877 10.1833 16.1238 9.57481 16.4163 8.67814C17.0456 6.75441 16.9047 5.43702 16.2643 4.58987C15.6381 3.76143 14.6875 3.5895 14.1627 3.5895C13.6555 3.5895 13.3235 3.89075 13.149 4.21465C12.9929 4.50432 12.9404 4.83899 12.9208 5.09751C12.9001 5.36964 12.9113 5.63254 12.9261 5.8212Z" stroke="white" stroke-width="1.3" stroke-linejoin="round"/>
<path d="M7.34175 23.2783C7.70504 23.2809 8.04815 23.132 8.27682 22.8488C8.50519 22.566 8.57817 22.2007 8.50503 21.8497C8.50503 21.8496 8.50503 21.8496 8.50503 21.8496L7.204 15.603L7.204 15.6029C7.06791 14.9497 6.44949 14.4357 5.77589 14.4357H3.29278C2.93392 14.4357 2.64296 14.7265 2.64278 15.0853L2.63906 22.6279C2.63898 22.8004 2.70742 22.9658 2.82933 23.0878C2.95124 23.2097 3.11662 23.2783 3.28906 23.2783H7.34175ZM7.34175 23.2783C7.34045 23.2783 7.33916 23.2782 7.33786 23.2782L7.34456 22.6283V23.2783H7.34175Z" stroke="white" stroke-width="1.3" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 2.8 KiB

@ -1,28 +1,15 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<title></title> <title></title>
</head> </head>
<body> <body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/oidc-client/1.11.5/oidc-client.min.js" <script src="https://cdnjs.cloudflare.com/ajax/libs/oidc-client/1.11.5/oidc-client.min.js" integrity="sha512-pGtU1n/6GJ8fu6bjYVGIOT9Dphaw5IWPwVlqkpvVgqBxFkvdNbytUh0H8AP15NYF777P4D3XEeA/uDWFCpSQ1g==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
integrity="sha512-pGtU1n/6GJ8fu6bjYVGIOT9Dphaw5IWPwVlqkpvVgqBxFkvdNbytUh0H8AP15NYF777P4D3XEeA/uDWFCpSQ1g=="
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script> <script>
new Oidc.UserManager().signinSilentCallback() new Oidc.UserManager().signinSilentCallback()
// обновляем рефреш токен в локалсторадже
// так как safari не дает доступ к кукам
.then(() => {
const refreshToken = localStorage.getItem('refresh_token');
if (refreshToken) {
localStorage.setItem('refresh_token', new URLSearchParams(document.location.search).get('refresh_token'));
}
})
.catch((err) => { .catch((err) => {
console.error('OIDC: silent refresh callback error', err); console.error('OIDC: silent refresh callback error', err);
}); });
</script> </script>
</body> </body>
</html> </html>

@ -7,6 +7,7 @@ import { insports } from './insports'
import { india } from './india' import { india } from './india'
import { tunisia } from './tunisia' import { tunisia } from './tunisia'
import { fqtv } from './fqtv' import { fqtv } from './fqtv'
import { rustat } from './rustat'
export const currentClient = process.env.REACT_APP_CLIENT || 'insports' export const currentClient = process.env.REACT_APP_CLIENT || 'insports'
@ -17,6 +18,7 @@ export const isFacrClient = currentClient === 'facr'
export const isIndiaClient = currentClient === 'india' export const isIndiaClient = currentClient === 'india'
export const isTunisClient = currentClient === 'tunisia' export const isTunisClient = currentClient === 'tunisia'
export const isFqtvClient = currentClient === 'fqtv' export const isFqtvClient = currentClient === 'fqtv'
export const isRustatClient = currentClient === 'rustat'
const clients = { const clients = {
facr, facr,
@ -25,6 +27,7 @@ const clients = {
insports, insports,
instat, instat,
lff, lff,
rustat,
tunisia, tunisia,
} }

@ -0,0 +1,53 @@
import { css } from 'styled-components/macro'
import {
ClientConfig,
ClientIds,
ClientNames,
} from './types'
export const rustat: ClientConfig = {
auth: {
clientId: ClientIds.Rustat,
},
currencyBadge: {
color: '#333333',
secondColor: 'rgba(255, 255, 255, 0.7)',
sign: 'Dollar',
},
defaultLanguage: 'en',
description: 'Live sports streaming platform. Football, basketball, ice hockey and more. Access to various player playlists and game highlights. Multiple subscription options. Available across all devices.',
disabledPreferences: true,
host: 'insports.tv',
name: ClientNames.Rustat,
privacyLink: '',
showSearch: true,
showSmartBanner: true,
styles: {
background: 'background-image: url(/images/Checker.png);',
homePageHeader: css`
background: #0B456E;
`,
logo: 'logo-rustat.svg',
logoHeight: 2.465,
logoLeft: 1.7,
logoTop: 1.5,
logoWidth: 6.37,
matchLogoHeight: 2.465,
matchLogoWidth: 6.37,
matchPageMobileHeaderLogo: css`
width: 90px;
height: 27px;
top: 0;
`,
mobileHeaderLogo: css`
width: 77px;
height: 24px;
`,
userAccountLogo: css`
width: 6.37rem;
height: 2.465rem;
`,
},
termsLink: '',
title: 'InSports TV - The Home of Sports Streaming',
}

@ -12,7 +12,8 @@ export enum ClientIds {
Insports = 'insports-ott-web', Insports = 'insports-ott-web',
Instat = 'ott-web', Instat = 'ott-web',
Lff = 'lff-ott-web', Lff = 'lff-ott-web',
Tunisia = 'tunisia-ott-web', Rustat = 'rustat-ott-web',
Tunisia = 'tunisia-ott-web'
} }
export enum ClientNames { export enum ClientNames {
@ -23,6 +24,7 @@ export enum ClientNames {
Insports = 'insports', // Глобал Insports = 'insports', // Глобал
Instat = 'instat', // Глобал Instat = 'instat', // Глобал
Lff = 'lff', // Латвия Lff = 'lff', // Латвия
Rustat = 'rustat', // Неизвесно
Tunisia = 'tunisia' // Тунис Tunisia = 'tunisia' // Тунис
} }

@ -155,11 +155,6 @@ const newDevicePopup = {
} }
export const indexLexics = { export const indexLexics = {
'1st_half': 1031,
'2nd_half': 1032,
'3rd_half': 20262,
'4th_half': 20263,
'5th_half': 20264,
add_to_favorites: 14967, add_to_favorites: 14967,
add_to_favorites_error: 12943, add_to_favorites_error: 12943,
all_competitions: 17926, all_competitions: 17926,
@ -175,9 +170,7 @@ export const indexLexics = {
cm: 817, cm: 817,
features: 13051, features: 13051,
football: 6958, football: 6958,
ft: 20261,
full_game: 13028, full_game: 13028,
full_time: 20258,
futsal: 17670, futsal: 17670,
game: 9680, game: 9680,
game_finished: 13026, game_finished: 13026,
@ -193,11 +186,9 @@ export const indexLexics = {
hide_score: 12982, hide_score: 12982,
highlights: 13033, highlights: 13033,
hockey: 6959, hockey: 6959,
ht: 20260,
interview: 13031, interview: 13031,
kg: 652, kg: 652,
kickoff_in: 13027, kickoff_in: 13027,
likes: 16628,
live: 13024, live: 13024,
loading: 3527, loading: 3527,
logout: 4306, logout: 4306,
@ -208,14 +199,11 @@ export const indexLexics = {
match_status_soon: 12986, match_status_soon: 12986,
match_video: 13025, match_video: 13025,
month_title: 2202, month_title: 2202,
my_likes: 20254,
no_match_access_body: 13419, no_match_access_body: 13419,
no_match_access_title: 13418, no_match_access_title: 13418,
player: 14975, player: 14975,
players: 164, players: 164,
players_video: 13032, players_video: 13032,
pm: 20259,
pre_match: 20257,
privacy_policy_and_statement: 15404, privacy_policy_and_statement: 15404,
round_highilights: 13050, round_highilights: 13050,
save: 828, save: 828,
@ -224,21 +212,15 @@ export const indexLexics = {
sport: 12993, sport: 12993,
team: 14973, team: 14973,
terms_and_conditions: 15738, terms_and_conditions: 15738,
timeline_title: 20266,
to_home: 13376, to_home: 13376,
total_likes: 20253,
tournament: 14974, tournament: 14974,
upcoming: 17925, upcoming: 17925,
user_account: 12928, user_account: 12928,
user_liked_this: 20267,
users_liked_this: 20255,
volleyball: 9761, volleyball: 9761,
watch_from_beginning: 13021, watch_from_beginning: 13021,
watch_from_last_pause: 13022, watch_from_last_pause: 13022,
watch_now: 13020, watch_now: 13020,
week_title: 6584, week_title: 6584,
you_and: 20256,
you_liked_this: 20265,
...filterPopup, ...filterPopup,
...confirmPopup, ...confirmPopup,

@ -26,4 +26,5 @@ export const payments: PaymentsType = {
[ClientNames.Facr]: PaymentSystem.Stripe, [ClientNames.Facr]: PaymentSystem.Stripe,
[ClientNames.Lff]: PaymentSystem.Stripe, [ClientNames.Lff]: PaymentSystem.Stripe,
[ClientNames.Fqtv]: PaymentSystem.Stripe, [ClientNames.Fqtv]: PaymentSystem.Stripe,
[ClientNames.Rustat]: PaymentSystem.Stripe,
} }

@ -44,12 +44,6 @@ const PAYMENT_APIS = {
staging: 'https://pay.test.insports.tv', staging: 'https://pay.test.insports.tv',
} }
const LIKES_API = {
preproduction: 'wss://ws.insports.tv/v1/events',
production: 'wss://ws.insports.tv/v1/events',
staging: 'wss://ws-test.insports.tv/v1/events',
}
const env = isProduction ? ENV : readSelectedApi() ?? ENV const env = isProduction ? ENV : readSelectedApi() ?? ENV
export const VIEWS_API = VIEWS_APIS[env] export const VIEWS_API = VIEWS_APIS[env]
@ -61,4 +55,3 @@ export const URL_AWS = 'https://cf-aws.insports.tv'
export const STATS_API_URL = STATS_APIS[env] export const STATS_API_URL = STATS_APIS[env]
export const ADS_API_URL = ADS_APIS[env] export const ADS_API_URL = ADS_APIS[env]
export const PAYMENT_API_URL = PAYMENT_APIS[env] export const PAYMENT_API_URL = PAYMENT_APIS[env]
export const LIKES_API_URL = LIKES_API[env]

@ -1,7 +1,7 @@
import { T9n } from 'features/T9n' import { T9n } from 'features/T9n'
import { client } from 'config/clients' import { client } from 'config/clients'
import { AUTH_SERVICE_OLD } from 'config/routes' import { AUTH_SERVICE } from 'config/routes'
import { import {
ScBody, ScBody,
@ -43,11 +43,11 @@ export const ConfirmPopup = (props: Props) => {
</ScText> </ScText>
<ScText> <ScText>
<T9n t='by_clicking' /> <T9n t='by_clicking' />
<ScLink href={`${AUTH_SERVICE_OLD}${client.termsLink}`} target='_blank' id='personal_t_k'> <ScLink href={`${AUTH_SERVICE}${client.termsLink}`} target='_blank' id='personal_t_k'>
<T9n t='terms_and_conditions' /> <T9n t='terms_and_conditions' />
</ScLink>&nbsp; </ScLink>&nbsp;
<T9n t='and' /> <T9n t='and' />
<ScLink href={`${AUTH_SERVICE_OLD}${client.privacyLink}`} target='_blank' id='personal_policy'> <ScLink href={`${AUTH_SERVICE}${client.privacyLink}`} target='_blank' id='personal_policy'>
<T9n t='privacy_policy_and_statement' /> <T9n t='privacy_policy_and_statement' />
</ScLink> </ScLink>
</ScText> </ScText>

@ -7,6 +7,7 @@ import { lff } from './lff'
import { india } from './india' import { india } from './india'
import { tunisia } from './tunisia' import { tunisia } from './tunisia'
import { fqtv } from './fqtv' import { fqtv } from './fqtv'
import { rustat } from './rustat'
export const clients = { export const clients = {
[ClientIds.Facr]: facr, [ClientIds.Facr]: facr,
@ -16,6 +17,7 @@ export const clients = {
[ClientIds.Insports]: insports, [ClientIds.Insports]: insports,
[ClientIds.India]: india, [ClientIds.India]: india,
[ClientIds.Tunisia]: tunisia, [ClientIds.Tunisia]: tunisia,
[ClientIds.Rustat]: rustat,
} }
const params = new URLSearchParams(window.location.search) const params = new URLSearchParams(window.location.search)

@ -0,0 +1,42 @@
import { css } from 'styled-components'
import { rustat as platformRustat } from 'config/clients/rustat'
import { isMobileDevice } from 'config/userAgent'
import { Background } from 'features/Background'
import type { ClientConfig } from './types'
export const rustat: ClientConfig = {
...platformRustat,
background: Background,
styles: {
centerBlock: css`
margin-top: 9.15rem;
${isMobileDevice ? css`
margin-top: 107px;
@media screen and (orientation: landscape) {
width: 290px;
margin: auto;
}
` : ''};
`,
logo: css`
background-image: url(/images/logo-rustat.svg);
background-position: center;
height: 85px;
width: 275px;
margin-bottom: 1.82rem;
${isMobileDevice ? css`
margin-bottom: 15px;
width: 165px;
height: 50px;
@media screen and (orientation: landscape){
width: 92px;
height: 22px;
}
` : ''}
`,
},
}

@ -34,7 +34,7 @@ export const lexics = {
go_back: 1907, go_back: 1907,
i_accept: 15737, i_accept: 15737,
i_agree: 15430, i_agree: 15430,
login: 20304, login: 13404,
ok: 724, ok: 724,
or_continue_with: 15118, or_continue_with: 15118,
password_changed_success: 17824, password_changed_success: 17824,

@ -33,6 +33,8 @@ export const getClientNameByRedirectUri = () => {
return ClientNames.Facr return ClientNames.Facr
case ClientNames.Instat: case ClientNames.Instat:
return ClientNames.Instat return ClientNames.Instat
case ClientNames.Rustat:
return 'tv.rustatsport.ru'
default: default:
return ClientNames.Insports return ClientNames.Insports
} }
@ -43,6 +45,7 @@ const clientsForRedirect = {
fqtv: ClientNames.Fqtv, fqtv: ClientNames.Fqtv,
india: ClientNames.India, india: ClientNames.India,
lff: ClientNames.Lff, lff: ClientNames.Lff,
rustat: ClientNames.Rustat,
tunisia: ClientNames.Tunisia, tunisia: ClientNames.Tunisia,
} }

@ -24,10 +24,6 @@ import {
setCookie, setCookie,
removeCookie, removeCookie,
isMatchPage, isMatchPage,
REFRESH_TOKEN_KEY,
removeRefreshToken,
writeRefreshToken,
readRefreshToken,
} from 'helpers' } from 'helpers'
import { import {
@ -81,7 +77,6 @@ export const useAuth = () => {
userManager.signoutRedirect({ post_logout_redirect_uri: urlWithLang }) userManager.signoutRedirect({ post_logout_redirect_uri: urlWithLang })
}) })
removeToken() removeToken()
removeRefreshToken()
if (key !== 'saveToken') { if (key !== 'saveToken') {
removeCookie('access_token') removeCookie('access_token')
} }
@ -163,15 +158,12 @@ export const useAuth = () => {
} }
} }
const signinRedirectCallback = useCallback((refreshToken: string | null) => { const signinRedirectCallback = useCallback(() => {
setPage(history.location.pathname) setPage(history.location.pathname)
userManager.signinRedirectCallback() userManager.signinRedirectCallback()
.then((loadedUser) => { .then((loadedUser) => {
storeUser(loadedUser) storeUser(loadedUser)
if (refreshToken) writeRefreshToken(refreshToken)
queryParamStorage.clear() queryParamStorage.clear()
if (page.includes(PAGES.useraccount)) { if (page.includes(PAGES.useraccount)) {
history.push(PAGES.home) history.push(PAGES.home)
@ -195,13 +187,10 @@ export const useAuth = () => {
const searchToken = urlSearch.get('access_token') const searchToken = urlSearch.get('access_token')
const searchRefToken = urlSearch.get('id_token') const searchRefToken = urlSearch.get('id_token')
const searchExp = urlSearch.get('expires_in') const searchExp = urlSearch.get('expires_in')
const refreshToken = urlSearch.get(REFRESH_TOKEN_KEY)
const isRedirectedBackFromAuthProvider = Boolean(searchToken && searchRefToken && searchExp) const isRedirectedBackFromAuthProvider = Boolean(searchToken && searchRefToken && searchExp)
isRedirectedBackFromAuthProvider isRedirectedBackFromAuthProvider ? signinRedirectCallback() : checkUser()
? signinRedirectCallback(refreshToken)
: checkUser()
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [ }, [
checkUser, checkUser,
@ -232,7 +221,7 @@ export const useAuth = () => {
}, [reChekNewDevice]) }, [reChekNewDevice])
useEffect(() => { useEffect(() => {
if (!needCheckNewDeviсe || !user) return undefined if (!needCheckNewDeviсe && !user) return undefined
const startCheckDevice = setInterval(checkNewDevice, 20000) const startCheckDevice = setInterval(checkNewDevice, 20000)
isNewDeviceLogin && clearInterval(startCheckDevice) isNewDeviceLogin && clearInterval(startCheckDevice)
return () => clearInterval(startCheckDevice) return () => clearInterval(startCheckDevice)
@ -242,7 +231,6 @@ export const useAuth = () => {
checkNewDevice, checkNewDevice,
isNewDeviceLogin, isNewDeviceLogin,
setIsNewDeviceLogin, setIsNewDeviceLogin,
user,
]) ])
duel.channel('active_page') // поле в LS, определяющее активность вкладки duel.channel('active_page') // поле в LS, определяющее активность вкладки
@ -252,13 +240,7 @@ export const useAuth = () => {
// библиотека oidc-client не поддерживает обновление токена только на 1 вкладке // библиотека oidc-client не поддерживает обновление токена только на 1 вкладке
// @ts-ignore // @ts-ignore
if (window.isMaster()) { if (window.isMaster()) {
// safari ограничивает доступ к куке через крос доменные запросы userManager.signinSilent().catch(logout)
// передаем рефреш токен через квери параметры
userManager.signinSilent({
extraQueryParams: {
refresh_token: readRefreshToken(),
},
}).catch(logout)
} }
} }
// если запросы вернули 401 | 403 // если запросы вернули 401 | 403

@ -1,7 +1,7 @@
import type { SubscriptionResponse, Subscription } from 'requests/getSubscriptions' import type { SubscriptionResponse, Subscription } from 'requests/getSubscriptions'
import type { LexicsId, Values } from 'features/LexicsStore/types' import type { LexicsId, Values } from 'features/LexicsStore/types'
import type { Match as MatchBase } from 'helpers/prepareMatches' import type { Match as MatchBase } from 'features/Matches/hooks'
import { MatchAccess } from 'features/Matches/helpers/getMatchClickAction' import { MatchAccess } from 'features/Matches/helpers/getMatchClickAction'
export enum Steps { export enum Steps {

@ -3,8 +3,6 @@ import addDays from 'date-fns/addDays'
import addMonths from 'date-fns/addMonths' import addMonths from 'date-fns/addMonths'
import startOfYear from 'date-fns/startOfYear' import startOfYear from 'date-fns/startOfYear'
import { isMobileDevice } from 'config'
type Args = { type Args = {
date: Date, date: Date,
lang: string, lang: string,
@ -22,7 +20,7 @@ const getMonthName = ({
export const getDisplayDate = ({ export const getDisplayDate = ({
date, date,
lang, lang,
monthType = isMobileDevice ? 'long' : 'short', monthType = 'long',
}: Args) => ({ }: Args) => ({
day: date.getDate(), day: date.getDate(),
month: getMonthName({ month: getMonthName({
@ -50,8 +48,6 @@ type Week = {
name: string, name: string,
} }
type Month = Week
export const getWeeks = (date: Date, locale: string) => { export const getWeeks = (date: Date, locale: string) => {
const weekStart = startOfWeek(date, { weekStartsOn: 1 }) const weekStart = startOfWeek(date, { weekStartsOn: 1 })
const getWeekDay = createDayGetter(weekStart, locale) const getWeekDay = createDayGetter(weekStart, locale)
@ -68,17 +64,17 @@ export const getWeeks = (date: Date, locale: string) => {
} }
const createMonthGetter = (yearStart: Date, locale: string) => (month: number) => { const createMonthGetter = (yearStart: Date, locale: string) => (month: number) => {
const monthDate = addMonths(yearStart, month) const dayDate = addMonths(yearStart, month)
return { return {
date: monthDate, date: dayDate,
name: new Intl.DateTimeFormat(locale || 'en', { month: 'short' }).format(monthDate), name: new Intl.DateTimeFormat(locale || 'en', { month: 'short' }).format(dayDate),
} }
} }
export const getMonths = (locale: string, date: Date) => { export const getMonths = (locale: string, date: Date) => {
const yearStart = startOfYear(date) const yearStart = startOfYear(date)
const getMonth = createMonthGetter(yearStart, locale) const getMonth = createMonthGetter(yearStart, locale)
const months: Array<Month> = [ const months: Array<Week> = [
getMonth(0), getMonth(0),
getMonth(1), getMonth(1),
getMonth(2), getMonth(2),

@ -29,8 +29,6 @@ import {
export const useDateFilter = () => { export const useDateFilter = () => {
const { const {
isMonthMode, isMonthMode,
isTimelineMode,
isWeekMode,
selectedDate, selectedDate,
selectedMode, selectedMode,
selectedMonthModeDate, selectedMonthModeDate,
@ -64,11 +62,8 @@ export const useDateFilter = () => {
lang, lang,
}) })
const filters = localStorage.getItem('filters') const filters = localStorage.getItem('filters')
const dateMode = localStorage.getItem('dateMode')
const parseFilters = filters && JSON.parse(filters) const parseFilters = filters && JSON.parse(filters)
const parseMode = dateMode && JSON.parse(dateMode)
const lastDate = parseFilters?.selectedDate const lastDate = parseFilters?.selectedDate
const lastMonthDate = new Date(parseMode?.selectedMonthModeDate)
const weekName = getWeekName(selectedDate, 'en') const weekName = getWeekName(selectedDate, 'en')
const validator = (value: unknown) => Boolean(value) && isObject(value) const validator = (value: unknown) => Boolean(value) && isObject(value)
@ -88,7 +83,6 @@ export const useDateFilter = () => {
useEffect(() => { useEffect(() => {
if (lastDate === selectedDate.getDate() if (lastDate === selectedDate.getDate()
&& lastMonthDate === selectedMonthModeDate
&& parseFilters && parseFilters
&& parseFilters.selectedLeague[0] !== 'all_competitions') { && parseFilters.selectedLeague[0] !== 'all_competitions') {
setIsShowTournament(false) setIsShowTournament(false)
@ -100,7 +94,7 @@ export const useDateFilter = () => {
setSelectedLeague(['all_competitions']) setSelectedLeague(['all_competitions'])
} }
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedDate, selectedMonthModeDate]) }, [selectedDate])
const onPreviousClick = () => { const onPreviousClick = () => {
addAdsViews() addAdsViews()
@ -136,8 +130,6 @@ export const useDateFilter = () => {
date, date,
isMonthMode, isMonthMode,
isOpen, isOpen,
isTimelineMode,
isWeekMode,
months, months,
onDateChange, onDateChange,
onNextClick, onNextClick,

@ -2,62 +2,40 @@ import { Fragment } from 'react'
import map from 'lodash/map' import map from 'lodash/map'
import { T9n } from 'features/T9n'
import { Icon } from 'features/Icon'
import { OutsideClick } from 'features/OutsideClick' import { OutsideClick } from 'features/OutsideClick'
import { BodyBackdrop } from 'features/PageLayout' import { BodyBackdrop } from 'features/PageLayout'
import { useHeaderFiltersStore } from 'features/HeaderFilters' import { Icon } from 'features/Icon'
import { isInSportsClient, isMobileDevice } from 'config'
import { useDateFilter } from './hooks' import { useDateFilter } from './hooks'
import { DatePicker } from '../DatePicker' import { DatePicker } from '../DatePicker'
import { Tabs } from '../../store/config'
import { import {
TabsList, Wrapper,
Tab, MonthWrapper,
TabTitle, WeekDaysWrapper,
YearWrapper,
ArrowButton, ArrowButton,
Arrow, Arrow,
MonthModeYear, DateButton,
MonthModeWrapper, MonthYear,
Month, Week,
MonthName,
CalendarWrapper,
DateWrapper,
MonthArrow,
WeekDaysWrapper,
WeekDay, WeekDay,
WeekName, WeekName,
WeekNumber, WeekNumber,
WeekModeButton,
WeekModeYear,
Week,
MonthButtonWrapper,
} from './styled' } from './styled'
import { useHeaderFiltersStore } from '../../store'
export const DateFilter = () => { export const DateFilter = () => {
const { const {
addAdsViews, addAdsViews,
close, close,
date, date,
isMonthMode,
isOpen, isOpen,
isWeekMode,
months,
onDateChange, onDateChange,
onNextClick, onNextClick,
onNextYearClick,
onPreviousClick, onPreviousClick,
onPrevYearClick,
onWeekDayClick, onWeekDayClick,
openDatePicker, openDatePicker,
selectedDate, selectedDate,
selectedMode,
selectedMonthModeDate,
setSelectedMode,
setSelectedMonthModeDate,
week, week,
} = useDateFilter() } = useDateFilter()
@ -66,97 +44,20 @@ export const DateFilter = () => {
} = useHeaderFiltersStore() } = useHeaderFiltersStore()
return ( return (
<CalendarWrapper isMonthMode={isMonthMode}> <Wrapper>
<DateWrapper isMonthMode={isMonthMode}> <MonthWrapper>
<MonthYear onClick={openDatePicker}>
{isWeekMode && (
<MonthButtonWrapper>
<WeekModeYear onClick={openDatePicker}>
{date.month} {' '} {date.year} {date.month} {' '} {date.year}
</WeekModeYear> </MonthYear>
<WeekModeButton isActive={isOpen} onClick={openDatePicker}> <DateButton
<Icon refIcon='Calendar' color='#fff' /> isActive={isOpen}
</WeekModeButton> onClick={openDatePicker}
</MonthButtonWrapper> id='main_calendar'
)}
{isMonthMode && (
<YearWrapper>
<MonthArrow
aria-label='Previous year'
onClick={onPrevYearClick}
>
<Arrow direction='left' />
</MonthArrow>
<MonthModeYear>
{selectedMonthModeDate.getFullYear()}
</MonthModeYear>
<MonthArrow
aria-label='Next year'
onClick={onNextYearClick}
>
<Arrow direction='right' />
</MonthArrow>
</YearWrapper>
)}
<TabsList>
{!isInSportsClient && (
<Tab
aria-pressed={selectedMode === Tabs.MONTH}
onClick={() => setSelectedMode(Tabs.MONTH)}
>
<TabTitle>
<T9n t='month_title' />
</TabTitle>
</Tab>
)}
{!isMobileDevice && (
<Tab
aria-pressed={selectedMode === Tabs.TIMELINE}
onClick={() => setSelectedMode(Tabs.TIMELINE)}
> >
<TabTitle> <Icon refIcon='Calendar' color='#fff' />
<T9n t='timeline_title' /> </DateButton>
</TabTitle> </MonthWrapper>
</Tab>
)}
{(!isMobileDevice || !isInSportsClient) && (
<Tab
aria-pressed={selectedMode === Tabs.WEEK}
onClick={() => setSelectedMode(Tabs.WEEK)}
>
<TabTitle>
<T9n t='week_title' />
</TabTitle>
</Tab>
)}
</TabsList>
</DateWrapper>
{isMonthMode && (
<MonthModeWrapper>
{
map(months, (month) => (
<Month
key={month.name}
selected={month.date.getMonth() === selectedMonthModeDate.getMonth()}
onClick={() => {
if (month.date.getMonth() !== selectedMonthModeDate.getMonth()) {
setSelectedMonthModeDate(month.date)
} else {
resetFilters()
}
}}
>
<MonthName>{month.name}</MonthName>
</Month>
))
}
</MonthModeWrapper>
)}
{isWeekMode && (
<WeekDaysWrapper> <WeekDaysWrapper>
<ArrowButton <ArrowButton
aria-label='Previous week' aria-label='Previous week'
@ -192,8 +93,6 @@ export const DateFilter = () => {
<Arrow direction='right' /> <Arrow direction='right' />
</ArrowButton> </ArrowButton>
</WeekDaysWrapper> </WeekDaysWrapper>
)}
{ {
isOpen && ( isOpen && (
<Fragment> <Fragment>
@ -208,6 +107,6 @@ export const DateFilter = () => {
</Fragment> </Fragment>
) )
} }
</CalendarWrapper> </Wrapper>
) )
} }

@ -2,7 +2,6 @@ import styled, { css } from 'styled-components/macro'
import { isMobileDevice } from 'config/userAgent' import { isMobileDevice } from 'config/userAgent'
import { devices } from 'config/devices' import { devices } from 'config/devices'
import { isInSportsClient } from 'config'
type Props = { type Props = {
isMonthMode: boolean, isMonthMode: boolean,
@ -18,6 +17,7 @@ export const BaseButton = styled.button`
export const Wrapper = styled.div` export const Wrapper = styled.div`
position: relative; position: relative;
/* width: 32.8rem; */
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
@ -29,6 +29,8 @@ export const Wrapper = styled.div`
${isMobileDevice ${isMobileDevice
? css` ? css`
/* padding-top: 4px; */
/* min-height: 84px; */
justify-content: space-between; justify-content: space-between;
@media (max-width: 450px){ @media (max-width: 450px){
@ -106,6 +108,7 @@ export const WeekDaysWrapper = styled.div`
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
margin-top: 0.567rem;
${isMobileDevice ${isMobileDevice
? css` ? css`
@ -116,6 +119,7 @@ export const WeekDaysWrapper = styled.div`
` `
export const Week = styled.div` export const Week = styled.div`
margin: 0 0.95rem;
display: flex; display: flex;
@media (max-width: 600px) { @media (max-width: 600px) {
@ -221,8 +225,11 @@ export const Arrow = styled.span<ArrowProps>`
: ''}; : ''};
` `
export const MonthModeWrapper = styled.div` export const MonthModeWrapper = styled(WeekDaysWrapper)<Props>`
display: flex; display: flex;
justify-content: center;
align-items: center;
margin-top: ${({ isMonthMode }) => (isMonthMode ? '0' : '0.2rem')};;
::-webkit-scrollbar { ::-webkit-scrollbar {
display: none; display: none;
@ -234,64 +241,47 @@ export const MonthModeWrapper = styled.div`
margin-top: 0; margin-top: 0;
overflow-y: auto; overflow-y: auto;
height: 100%; height: 100%;
& > :not(:last-child) {
margin-right: 25px;
}
` `
: ''}; : ''};
` `
export const CalendarWrapper = styled(Wrapper)<Props>` export const FacrWrapper = styled(Wrapper)<Props>`
display: flex; justify-content: space-between;
flex-direction: column; height: ${(isMobileDevice ? '100%' : 'fit-content')};
height: fit-content; width: ${({ isMonthMode }) => (isMonthMode ? '49.5%' : '26.3rem')};
padding-top: 2rem;
position: relative;
@media (max-width: 450px){ @media (max-width: 450px){
width: 100%; width: 100%;
justify-content: flex-start; justify-content: flex-start;
padding-top: 0;
}; };
` `
export const WeekModeButton = styled(DateButton)` export const FacrDateButton = styled(DateButton)`
right: 4.5rem; left: 24rem;
top: -5px; top: 0;
${isMobileDevice
? css`
position: static;
`
: ''};
` `
export const MonthButtonWrapper = styled.div` export const FacrMonthWrapper = styled(MonthWrapper)`
${() => {
if (isMobileDevice) {
if (isInSportsClient) {
return css`
position: static; position: static;
display: flex; width: auto;
align-items: baseline; align-self: auto;
justify-content: center;
` ${isMobileDevice
} ? css`
return css`
display: flex;
position: absolute; position: absolute;
left: 50%; left: 50%;
transform: translateX(-50%); transform: translateX(-50%);
` `
} : ''};
return ''
}}
` `
export const DateWrapper = styled.div<Props>` export const DateWrapper = styled.div<Props>`
position: relative; position: relative;
margin-bottom: 0.7rem; display: flex;
align-items: flex-end;
justify-content: space-between;
width: ${({ isMonthMode }) => (isMonthMode ? '25rem' : '16.9rem')};
margin: ${({ isMonthMode }) => (isMonthMode ? '3.7rem 0 0.7rem' : '3rem 0 0')};
${isMobileDevice ${isMobileDevice
? css` ? css`
@ -310,18 +300,24 @@ export const MonthArrow = styled(ArrowButton)`
height: auto; height: auto;
` `
export const Months = styled(Week)`` export const Months = styled(Week)`
height: 100%;
width: 100%;
margin: 0;
justify-content: space-between;
`
export const FacrWeek = styled(Week)` export const FacrWeek = styled(Week)`
margin: 0 1.8rem; margin: 0 1.8rem;
` `
export const Month = styled(WeekDay)` export const Month = styled(WeekDay)`
width: 3.5rem; width: auto;
${isMobileDevice ${isMobileDevice
? css` ? css`
margin-top: 10px; margin-top: 10px;
margin-right: 25px;
min-width: fit-content; min-width: fit-content;
` `
: ''}; : ''};
@ -339,36 +335,13 @@ export const MonthModeYear = styled(MonthYear)`
: ''}; : ''};
` `
export const WeekModeYear = styled(MonthYear)`
line-height: normal;
font-size: 1rem;
position: absolute;
left: 2.5rem;
cursor: pointer;
display: flex;
align-items: center;
height: 100%;
${isMobileDevice
? css`
position: static;
font-size: 10px;
`
: ''};
`
export const TabsList = styled.div` export const TabsList = styled.div`
display: flex; display: flex;
justify-content: center; justify-content: space-between;
margin: 0 auto 0; height: fit-content;
& > :not(:last-child) {
margin-right: 2rem;
}
${isMobileDevice ${isMobileDevice
? css` ? css`
justify-content: flex-start;
top: 3px; top: 3px;
padding: 0; padding: 0;
` `
@ -376,9 +349,6 @@ export const TabsList = styled.div`
` `
export const YearWrapper = styled.div` export const YearWrapper = styled.div`
position: absolute;
right: 0;
top: -5px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@ -390,7 +360,6 @@ export const YearWrapper = styled.div`
position: absolute; position: absolute;
left: 50%; left: 50%;
transform: translateX(-50%); transform: translateX(-50%);
top: -1px;
` `
: ''}; : ''};
` `
@ -418,13 +387,17 @@ export const Tab = styled.button`
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
font-size: 0.85rem; font-size: .75rem;
font-weight: 600; font-weight: 600;
cursor: pointer; cursor: pointer;
border: none; border: none;
background: none; background: none;
padding: 0; padding: 0;
:first-child {
margin-right: 2rem;
}
${isMobileDevice ${isMobileDevice
? css` ? css`
font-size: 10px; font-size: 10px;

@ -0,0 +1,193 @@
import map from 'lodash/map'
import { T9n } from 'features/T9n'
import { Icon } from 'features/Icon'
import { OutsideClick } from 'features/OutsideClick'
import { BodyBackdrop } from 'features/PageLayout'
import { useHeaderFiltersStore } from 'features/HeaderFilters'
import { Fragment } from 'react'
import { useDateFilter } from '../DateFilter/hooks'
import { DatePicker } from '../DatePicker'
import { Tabs } from '../../store/config'
import {
TabsList,
Tab,
TabTitle,
MonthsMode,
YearWrapper,
ArrowButton,
Arrow,
MonthModeYear,
MonthModeWrapper,
Months,
Month,
MonthName,
FacrWrapper,
DateWrapper,
MonthArrow,
WeekDaysWrapper,
FacrMonthWrapper,
WeekDay,
WeekName,
WeekNumber,
FacrDateButton,
FacrWeek,
} from '../DateFilter/styled'
export const FacrDateFilter = () => {
const {
addAdsViews,
close,
date,
isMonthMode,
isOpen,
months,
onDateChange,
onNextClick,
onNextYearClick,
onPreviousClick,
onPrevYearClick,
onWeekDayClick,
openDatePicker,
selectedDate,
selectedMode,
selectedMonthModeDate,
setSelectedMode,
setSelectedMonthModeDate,
week,
} = useDateFilter()
const {
resetFilters,
} = useHeaderFiltersStore()
return (
<FacrWrapper isMonthMode={isMonthMode}>
<DateWrapper isMonthMode={isMonthMode}>
<TabsList>
<Tab
aria-pressed={selectedMode === Tabs.MONTH}
onClick={() => setSelectedMode(Tabs.MONTH)}
>
<TabTitle>
<T9n t='month_title' />
</TabTitle>
</Tab>
<Tab
aria-pressed={selectedMode === Tabs.WEEK}
onClick={() => setSelectedMode(Tabs.WEEK)}
>
<TabTitle>
<T9n t='week_title' />
</TabTitle>
</Tab>
</TabsList>
{isMonthMode
? (
<YearWrapper>
<MonthArrow
aria-label='Previous year'
onClick={onPrevYearClick}
>
<Arrow direction='left' />
</MonthArrow>
<MonthModeYear>
{selectedMonthModeDate.getFullYear()}
</MonthModeYear>
<MonthArrow
aria-label='Next year'
onClick={onNextYearClick}
>
<Arrow direction='right' />
</MonthArrow>
</YearWrapper>
)
: (
<FacrMonthWrapper>
<MonthModeYear onClick={openDatePicker}>
{date.month} {' '} {date.year}
</MonthModeYear>
<FacrDateButton isActive={isOpen} onClick={openDatePicker}>
<Icon refIcon='Calendar' color='#fff' />
</FacrDateButton>
</FacrMonthWrapper>
)}
</DateWrapper>
{isMonthMode
? (
<MonthsMode>
<MonthModeWrapper isMonthMode={isMonthMode}>
<Months>
{
map(months, (day) => (
<Month
key={day.name}
selected={day.date.getMonth() === selectedMonthModeDate.getMonth()}
onClick={() => setSelectedMonthModeDate(day.date)}
>
<MonthName>{day.name}</MonthName>
</Month>
))
}
</Months>
</MonthModeWrapper>
</MonthsMode>
)
: (
<WeekDaysWrapper>
<ArrowButton
aria-label='Previous week'
onClick={onPreviousClick}
>
<Arrow direction='left' />
</ArrowButton>
<FacrWeek>
{
map(week, (day) => (
<WeekDay
key={day.name}
selected={day.date.getDate() === selectedDate.getDate()}
onClick={() => {
if (day.date.getDate() !== selectedDate.getDate()) {
addAdsViews()
onWeekDayClick(day.date)
} else {
resetFilters()
}
}}
>
<WeekName>{day.name.slice(0, 3)}</WeekName>
<WeekNumber>{day.date.getDate()}</WeekNumber>
</WeekDay>
))
}
</FacrWeek>
<ArrowButton
aria-label='Next week'
onClick={onNextClick}
>
<Arrow direction='right' />
</ArrowButton>
</WeekDaysWrapper>
)}
{
isOpen && (
<Fragment>
<OutsideClick onClick={close}>
<DatePicker
open
selected={selectedDate}
onChange={onDateChange}
/>
</OutsideClick>
<BodyBackdrop />
</Fragment>
)
}
</FacrWrapper>
)
}

@ -1,2 +1,3 @@
export * from './components/DateFilter' export * from './components/DateFilter'
export * from './components/FacrDateFilter'
export * from './store' export * from './store'

@ -8,5 +8,4 @@ export const filterKeys = {
export enum Tabs { export enum Tabs {
WEEK, WEEK,
MONTH, MONTH,
TIMELINE
} }

@ -14,12 +14,12 @@ import { useQueryParamStore } from 'hooks'
import { getSportLexic } from 'helpers' import { getSportLexic } from 'helpers'
import { isMobileDevice, querieKeys } from 'config' import { querieKeys } from 'config'
import { getLocalStorageItem } from 'helpers/getLocalStorage' import { getLocalStorageItem } from 'helpers/getLocalStorage'
import type { Match } from 'helpers'
import { filterKeys, Tabs } from '../config' import { filterKeys, Tabs } from '../config'
import { isValidDate } from '../helpers/isValidDate' import { isValidDate } from '../helpers/isValidDate'
import type { Match } from '../../../Matches'
export const useFilters = () => { export const useFilters = () => {
const { search } = useLocation() const { search } = useLocation()
@ -31,11 +31,9 @@ export const useFilters = () => {
}) })
const sportList = getLocalStorageItem(querieKeys.sportsList) const sportList = getLocalStorageItem(querieKeys.sportsList)
const [selectedMode, setSelectedMode] = useState<Tabs>(Tabs.WEEK) const [selectedMode, setSelectedMode] = useState(0)
const [selectedMonthModeDate, setSelectedMonthModeDate] = useState(startOfMonth(new Date())) const [selectedMonthModeDate, setSelectedMonthModeDate] = useState(startOfMonth(new Date()))
const isMonthMode = selectedMode === Tabs.MONTH const isMonthMode = selectedMode === Tabs.MONTH
const isTimelineMode = selectedMode === Tabs.TIMELINE && !isMobileDevice
const isWeekMode = selectedMode === Tabs.WEEK
const [selectedSport, setSelectedSport] = useState(['all_sports']) const [selectedSport, setSelectedSport] = useState(['all_sports'])
const [selectedLeague, setSelectedLeague] = useState<Array<string | number>>(['all_competitions']) const [selectedLeague, setSelectedLeague] = useState<Array<string | number>>(['all_competitions'])
@ -63,11 +61,11 @@ export const useFilters = () => {
) )
}, [selectedMode, selectedMonthModeDate]) }, [selectedMode, selectedMonthModeDate])
const compareSport = useCallback((sportType: number, sportNames: Array<string>) => { const compareSport = useCallback((match: Match, sportNames: Array<string>) => {
if (sportNames[0] === 'all_sports') { if (sportNames[0] === 'all_sports') {
return true return true
} }
const sport = getSportLexic(sportType) const sport = getSportLexic(match.sportType)
return (sportNames.indexOf(sport) >= 0 || sportNames.indexOf(`${sport}_popup`) >= 0) return (sportNames.indexOf(sport) >= 0 || sportNames.indexOf(`${sport}_popup`) >= 0)
}, []) }, [])
@ -119,9 +117,7 @@ export const useFilters = () => {
compareSport, compareSport,
isMonthMode, isMonthMode,
isShowTournament, isShowTournament,
isTimelineMode,
isTodaySelected, isTodaySelected,
isWeekMode,
resetFilters, resetFilters,
selectTournament, selectTournament,
selectedDate, selectedDate,
@ -167,8 +163,6 @@ export const useFilters = () => {
setSelectedMonthModeDate, setSelectedMonthModeDate,
selectedMonthModeDate, selectedMonthModeDate,
isMonthMode, isMonthMode,
isTimelineMode,
isWeekMode,
]) ])
return store return store

@ -3,11 +3,12 @@ import { useRecoilValue } from 'recoil'
import { isAndroid, isIOS } from 'config/userAgent' import { isAndroid, isIOS } from 'config/userAgent'
import { import {
client, client,
isFqtvClient,
isLffClient, isLffClient,
} from 'config/clients' } from 'config/clients'
import { HeaderMenu } from 'features/HeaderMenu' import { HeaderMenu } from 'features/HeaderMenu'
import { DateFilter } from 'features/HeaderFilters' import { DateFilter, FacrDateFilter } from 'features/HeaderFilters'
import { ScoreSwitch } from 'features/MatchSwitches' import { ScoreSwitch } from 'features/MatchSwitches'
import { SportsFilter } from 'features/SportsFilter' import { SportsFilter } from 'features/SportsFilter'
import { isSportFilterShownAtom } from 'features/HomePage/Atoms/HomePageAtoms' import { isSportFilterShownAtom } from 'features/HomePage/Atoms/HomePageAtoms'
@ -44,7 +45,7 @@ export const HeaderMobile = ({
} }
<HeaderStyled> <HeaderStyled>
<HeaderMenu /> <HeaderMenu />
<DateFilter /> {isFqtvClient ? <FacrDateFilter /> : <DateFilter />}
<ScSportsWrapper> <ScSportsWrapper>
{!isLffClient && isSportFilterShown ? <SportsFilter /> : null} {!isLffClient && isSportFilterShown ? <SportsFilter /> : null}
<ScoreSwitchWrapper> <ScoreSwitchWrapper>

@ -21,6 +21,7 @@ export const defaultHeaderStyles = (
ClientNames.Tunisia, ClientNames.Tunisia,
ClientNames.Facr, ClientNames.Facr,
ClientNames.Fqtv, ClientNames.Fqtv,
ClientNames.Rustat,
].includes(client.name)) { ].includes(client.name)) {
return client.styles.homePageHeader return client.styles.homePageHeader
} }
@ -74,7 +75,6 @@ export const HeaderStyled = styled.header<HeaderProps>`
? css` ? css`
padding: 8px; padding: 8px;
margin-bottom: 50px; margin-bottom: 50px;
justify-content: flex-start;
` `
: ''} : ''}
` `

@ -1,7 +1,7 @@
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
import { PAGES } from 'config/pages' import { PAGES } from 'config/pages'
import { client } from 'config/clients' import { client, isFqtvClient } from 'config/clients'
import { Menu } from 'features/Menu' import { Menu } from 'features/Menu'
import { ScoreSwitch } from 'features/MatchSwitches' import { ScoreSwitch } from 'features/MatchSwitches'
@ -9,6 +9,7 @@ import { Search } from 'features/Search'
import { import {
DateFilter, DateFilter,
useHeaderFiltersStore, useHeaderFiltersStore,
FacrDateFilter,
} from 'features/HeaderFilters' } from 'features/HeaderFilters'
import { import {
@ -44,7 +45,7 @@ export const Header = () => {
<Search /> <Search />
</HeaderGroup> </HeaderGroup>
</Position> </Position>
<DateFilter /> {isFqtvClient ? <FacrDateFilter /> : <DateFilter />}
<Position right={0.71}> <Position right={0.71}>
<HeaderGroup> <HeaderGroup>
<ScoreSwitch /> <ScoreSwitch />

@ -22,7 +22,6 @@ import { isSportFilterShownAtom } from '../../Atoms/HomePageAtoms'
export const HeaderFilters = () => { export const HeaderFilters = () => {
const { const {
isShowTournament, isShowTournament,
isTimelineMode,
selectedFilters, selectedFilters,
selectTournament, selectTournament,
setIsShowTournament, setIsShowTournament,
@ -72,7 +71,7 @@ export const HeaderFilters = () => {
)} )}
{!isLffClient && isShowTournament && isSportFilterShown && <SportsFilter />} {!isLffClient && isShowTournament && isSportFilterShown && <SportsFilter />}
{isShowTournament && !isTimelineMode && ( {isShowTournament && (
<ScFilterItemsWrap> <ScFilterItemsWrap>
<ScFilterItem <ScFilterItem
className={isActiveFilter('live') ? 'activeLive' : ''} className={isActiveFilter('live') ? 'activeLive' : ''}

@ -6,6 +6,7 @@ export const ScHeaderFilters = styled.div`
display: flex; display: flex;
flex-direction: row; flex-direction: row;
margin-bottom: 23px; margin-bottom: 23px;
align-items: center;
.activeLive { .activeLive {
color: #ffffff; color: #ffffff;
@ -33,7 +34,7 @@ type Props = {
export const ScFilterItem = styled.div<Props>` export const ScFilterItem = styled.div<Props>`
text-transform: uppercase; text-transform: uppercase;
font-weight: 700; font-weight: 700;
font-size: 0.75rem; font-size: 18px;
color: rgba(255, 255, 255, 0.5); color: rgba(255, 255, 255, 0.5);
margin: 0 10px; margin: 0 10px;
cursor: pointer; cursor: pointer;

@ -6,7 +6,6 @@ import { ConfirmPopup } from 'features/AuthServiceApp/components/ConfirmPopup'
import { Matches } from 'features/Matches' import { Matches } from 'features/Matches'
import { import {
HeaderFiltersStore, HeaderFiltersStore,
useHeaderFiltersStore,
} from 'features/HeaderFilters' } from 'features/HeaderFilters'
import { import {
PageWrapper, PageWrapper,
@ -15,7 +14,6 @@ import {
} from 'features/PageLayout' } from 'features/PageLayout'
import { UserFavorites } from 'features/UserFavorites' import { UserFavorites } from 'features/UserFavorites'
import { BuyMatchPopup } from 'features/BuyMatchPopup' import { BuyMatchPopup } from 'features/BuyMatchPopup'
import { MatchesTimeline } from 'features/MatchesTimeline'
import { HEADER_MOBILE_ADS } from 'components/Ads/types' import { HEADER_MOBILE_ADS } from 'components/Ads/types'
import { HeaderAds } from 'components/Ads' import { HeaderAds } from 'components/Ads'
@ -37,8 +35,6 @@ const Home = () => {
userInfo, userInfo,
} = useHomePage() } = useHomePage()
const { isTimelineMode } = useHeaderFiltersStore()
return ( return (
<PageWrapper> <PageWrapper>
{isMobileDevice ? ( {isMobileDevice ? (
@ -64,9 +60,7 @@ const Home = () => {
} }
/> />
)} )}
{isTimelineMode <Matches fetch={fetchMatches} />
? <MatchesTimeline />
: <Matches fetch={fetchMatches} />}
<ConfirmPopup <ConfirmPopup
isModalOpen={isShowConfirmPopup} isModalOpen={isShowConfirmPopup}
handleModalClose={handleCloseConfirmPopup} handleModalClose={handleCloseConfirmPopup}

@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import React, { CSSProperties } from 'react' import React, { CSSProperties } from 'react'
import * as icons from '../../libs/index' import * as icons from '../../libs/index'
@ -6,7 +7,7 @@ export type IconProps = {
color?: string, color?: string,
direction?: number, direction?: number,
onClick?: () => void, onClick?: () => void,
refIcon: keyof typeof icons | string, refIcon: any,
size?: number | string, size?: number | string,
styles?: CSSProperties, styles?: CSSProperties,
} }

@ -9,7 +9,7 @@ import {
client, client,
} from 'config' } from 'config'
import type { Match } from 'helpers' import type { Match } from 'features/Matches'
import { useMatchSwitchesStore } from 'features/MatchSwitches' import { useMatchSwitchesStore } from 'features/MatchSwitches'
import { useName } from 'features/Name' import { useName } from 'features/Name'
import { T9n } from 'features/T9n' import { T9n } from 'features/T9n'

@ -4,20 +4,20 @@ import {
useState, useState,
} from 'react' } from 'react'
import { readToken } from 'helpers'
export type TUseCardFrontside = { export type TUseCardFrontside = {
preview?: string, preview?: string,
previewURL?: string, previewURL?: string,
} }
const PREVIEW_WIDTH = 400 // макс. 1920
export const useCardPreview = ({ export const useCardPreview = ({
preview, preview,
previewURL, previewURL,
}: TUseCardFrontside) => { }: TUseCardFrontside) => {
const [previewImage, setPreviewImage] = useState('') const [previewImage, setPreviewImage] = useState('')
const currentPreviewURL = useMemo(() => ( const currentPreviewURL = useMemo(() => (
previewURL ? `${previewURL}?width=${PREVIEW_WIDTH}` : preview previewURL ? `${previewURL}?access_token=${readToken()}` : preview
), [preview, previewURL]) ), [preview, previewURL])
useEffect(() => { useEffect(() => {
@ -25,8 +25,9 @@ export const useCardPreview = ({
if (!currentPreviewURL) return if (!currentPreviewURL) return
try { try {
const image = await fetch(String(currentPreviewURL)) const image = await fetch(String(currentPreviewURL), {
.then(async (result) => ({ headers: { Authorization: `Bearer ${readToken()}` },
}).then(async (result) => ({
blob: await result.blob(), blob: await result.blob(),
status: result.status, status: result.status,
})) }))

@ -8,7 +8,7 @@ import { client } from 'config/clients'
import type { LiveScore } from 'requests' import type { LiveScore } from 'requests'
import type { Match } from 'helpers' import type { Match } from 'features/Matches'
import { useMatchSwitchesStore } from 'features/MatchSwitches' import { useMatchSwitchesStore } from 'features/MatchSwitches'
import { useName } from 'features/Name' import { useName } from 'features/Name'
import { T9n } from 'features/T9n' import { T9n } from 'features/T9n'
@ -82,7 +82,7 @@ export const CardFrontside = ({
const tournamentName = useName(tournament) const tournamentName = useName(tournament)
const { isInFavorites } = useUserFavoritesStore() const { isInFavorites } = useUserFavoritesStore()
const { isScoreHidden } = useMatchSwitchesStore() const { isScoreHidden } = useMatchSwitchesStore()
const { isMonthMode, isTimelineMode } = useHeaderFiltersStore() const { isMonthMode } = useHeaderFiltersStore()
const { color } = tournament const { color } = tournament
const isInFuture = getUnixTime(date) > getUnixTime(new Date()) const isInFuture = getUnixTime(date) > getUnixTime(new Date())
const showScore = !( const showScore = !(
@ -179,9 +179,9 @@ export const CardFrontside = ({
<MatchDate <MatchDate
isHomePage={isHomePage} isHomePage={isHomePage}
isMatchPage={isMatchPage} isMatchPage={isMatchPage}
isMonthMode={isMonthMode || isTimelineMode} isMonthMode={isMonthMode}
> >
{(isHomePage && !isMonthMode && !isTimelineMode) || isMatchPage ? null : prepareDate} {(isHomePage && !isMonthMode) || isMatchPage ? null : prepareDate}
<Time>{prepareTime}</Time> <Time>{prepareTime}</Time>
</MatchDate> </MatchDate>
{live && ( {live && (

@ -1,3 +1,3 @@
export const MATCH_CARD_WIDTH = 22 export const MATCH_CARD_WIDTH = 12
export const MATCH_CARD_GAP = 40 export const MATCH_CARD_GAP = 20

@ -10,7 +10,7 @@ import {
ProfileTypes, ProfileTypes,
} from 'config' } from 'config'
import type { Match } from 'helpers' import type { Match } from 'features/Matches'
import { useMatchPopupStore } from 'features/MatchPopup' import { useMatchPopupStore } from 'features/MatchPopup'
import { useBuyMatchPopupStore } from 'features/BuyMatchPopup' import { useBuyMatchPopupStore } from 'features/BuyMatchPopup'
import { useAuthStore } from 'features/AuthStore' import { useAuthStore } from 'features/AuthStore'

@ -1,4 +1,4 @@
import type { Match } from 'helpers' import type { Match } from 'features/Matches'
import { isMobileDevice } from 'config/userAgent' import { isMobileDevice } from 'config/userAgent'

@ -1,58 +0,0 @@
import { atom, selector } from 'recoil'
export interface RawLikeEvent {
e: number,
episode: number,
likes: number,
s: number,
}
export interface LikeEvent extends RawLikeEvent {
iLiked: boolean,
}
export const myLikesState = atom<Array<RawLikeEvent>>({
default: [],
key: 'myLikesState',
})
export const totalLikesState = atom<Array<RawLikeEvent>>({
default: [],
key: 'totalLikesState',
})
const transformedLikesState = selector({
get: ({ get }) => {
const totalLikes = get(totalLikesState)
const myLikes = get(myLikesState)
const likes: Array<LikeEvent> = totalLikes.map((like) => ({
...like,
iLiked: myLikes.findIndex(({ episode }) => episode === like.episode) !== -1,
}))
return likes
},
key: 'transformedLikesState',
})
export const sortTypeState = atom<'asc' | 'desc'>({
default: 'asc',
key: 'sortTypeState',
})
export const filterTypeState = atom<'myLikes' | 'totalLikes'>({
default: 'totalLikes',
key: 'filterTypeState',
})
export const filteredLikesState = selector<Array<LikeEvent>>({
get: ({ get }) => {
const likes = get(transformedLikesState)
const filterType = get(filterTypeState)
return filterType === 'totalLikes' ? likes : likes.filter(({ iLiked }) => iLiked)
},
key: 'filteredLikesState',
})

@ -1,75 +0,0 @@
import { useEffect } from 'react'
import { useSetRecoilState } from 'recoil'
import { LIKES_API_URL } from 'config'
import { useAuthStore } from 'features/AuthStore'
import { FULL_MATCH_BOUNDARY } from 'features/MatchPage/components/LiveMatch/helpers'
import {
usePageParams,
useVideoBounds,
useWebSocket,
} from 'hooks'
import {
myLikesState,
totalLikesState,
type RawLikeEvent,
} from '../atoms'
import { useMatchPageStore } from '..'
interface Data {
data: Array<RawLikeEvent>,
type: 'user_likes' | 'total_likes',
}
export const useLikes = () => {
const setTotalLikes = useSetRecoilState(totalLikesState)
const setMyLikes = useSetRecoilState(myLikesState)
const { playingProgress } = useMatchPageStore()
const videoBounds = useVideoBounds()
const { profileId, sportType } = usePageParams()
const { user } = useAuthStore()
useEffect(() => {
setTotalLikes([])
setMyLikes([])
}, [setMyLikes, setTotalLikes, profileId, sportType])
const url = `${LIKES_API_URL}?sport_id=${sportType}&match_id=${profileId}&access_token=${user?.access_token}`
const { sendMessage, webSocketState } = useWebSocket<Data>({
allowConnection: Boolean(user),
autoReconnect: true,
handlers: {
onMessage: ({ data = [], type }) => {
type === 'total_likes' && setTotalLikes(data)
type === 'user_likes' && setMyLikes(data)
},
},
maxReconnectAttempts: 10,
url,
})
const likeClick = () => {
const startSecond = Number(videoBounds?.find(({ h }) => h === FULL_MATCH_BOUNDARY)?.s || 0)
const message = {
data: {
second: playingProgress + startSecond,
},
event: 'like',
}
sendMessage(message)
}
return {
canLike: user && webSocketState === WebSocket.OPEN,
likeClick,
}
}

@ -9,8 +9,8 @@ import sortedUniq from 'lodash/sortedUniq'
import isNull from 'lodash/isNull' import isNull from 'lodash/isNull'
import sortBy from 'lodash/sortBy' import sortBy from 'lodash/sortBy'
import type { Match } from 'helpers' import type { Match } from 'features/Matches'
import { prepareMatches } from 'helpers' import { prepareMatches } from 'features/Matches/helpers/prepareMatches'
import { useAuthStore } from 'features/AuthStore' import { useAuthStore } from 'features/AuthStore'
import type { MatchInfo } from 'requests' import type { MatchInfo } from 'requests'

@ -6,8 +6,6 @@ import {
import { useMatchPage } from './hooks' import { useMatchPage } from './hooks'
export * from './atoms'
type Context = ReturnType<typeof useMatchPage> type Context = ReturnType<typeof useMatchPage>
type Props = { children: ReactNode } type Props = { children: ReactNode }

@ -1,7 +1,6 @@
import type { Lexics, Episodes } from 'requests' import type { Lexics, Episodes } from 'requests'
import type { Match } from 'helpers' import type { Match } from 'features/Matches'
import { Tabs } from 'features/MatchSidePlaylists/config' import { Tabs } from 'features/MatchSidePlaylists/config'
import type { MatchPlaylistIds } from './helpers/buildPlaylists' import type { MatchPlaylistIds } from './helpers/buildPlaylists'

@ -1,5 +1,5 @@
import type { Match } from 'helpers' import type { Match } from 'features/Matches/hooks'
export type MatchData = Pick<Match, ( export type MatchData = Pick<Match, (
'calc' 'calc'

@ -1,126 +0,0 @@
import { Fragment } from 'react'
import { useVideoBounds } from 'hooks'
import type { LikeEvent as LikeEventType } from 'features/MatchPage/store/atoms'
import { T9n } from 'features/T9n'
import { useMatchPageStore } from 'features/MatchPage/store'
import { Tabs } from 'features/MatchSidePlaylists/config'
import { FULL_MATCH_BOUNDARY } from 'features/MatchPage/components/LiveMatch/helpers'
import { PlaylistTypes } from 'features/MatchPage/types'
import {
Title,
LikeIcon,
Time,
Button,
LikesCount,
} from './styled'
type Props = {
groupTitle: string,
likeEvent: LikeEventType,
}
const groupTitlesMap: Record<string, string> = {
full_time: 'ft',
half_time: 'ht',
pre_match: 'pm',
}
export const LikeEvent = ({
groupTitle,
likeEvent,
}: Props) => {
const videoBounds = useVideoBounds()
const { handlePlaylistClick, selectedPlaylist } = useMatchPageStore()
const firstBound = videoBounds?.find(({ h }) => h === FULL_MATCH_BOUNDARY)
const half = groupTitle.match(/\d/)
const startSecond = half
? likeEvent.s - Number(videoBounds?.find(({ h }) => h === half[0])?.s || 0)
: 0
const startMinute = Math.ceil(startSecond / 60)
const active = selectedPlaylist.id === likeEvent.episode && selectedPlaylist.tab === Tabs.LIKES
const handleClick = () => {
handlePlaylistClick({
playlist: {
episodes: [{
c: Math.ceil((likeEvent.e - likeEvent.s) / 12),
e: likeEvent.e - Number(firstBound?.s || 0),
h: 0,
s: likeEvent.s - Number(firstBound?.s || 0),
}],
id: likeEvent.episode,
type: PlaylistTypes.EVENT,
},
tab: Tabs.LIKES,
})
}
const getTitle = () => {
switch (true) {
case likeEvent.iLiked && likeEvent.likes === 1:
return (
<T9n t='you_liked_this' />
)
case likeEvent.iLiked && likeEvent.likes === 2:
return (
<Fragment>
<T9n t='you_and' /> <LikesCount>1</LikesCount> <T9n t='user_liked_this' />
</Fragment>
)
case likeEvent.iLiked && likeEvent.likes > 2:
return (
<Fragment>
<T9n t='you_and' /> <LikesCount>{likeEvent.likes - 1}</LikesCount> <T9n t='users_liked_this' />
</Fragment>
)
case !likeEvent.iLiked && likeEvent.likes === 1:
return (
<Fragment>
<LikesCount>1</LikesCount> <T9n t='user_liked_this' />
</Fragment>
)
case !likeEvent.iLiked && likeEvent.likes > 1:
return (
<Fragment>
<LikesCount>{likeEvent.likes}</LikesCount> <T9n t='users_liked_this' />
</Fragment>
)
default: return null
}
}
return (
<Button
onClick={handleClick}
active={active}
groupTitle={groupTitle}
>
{likeEvent.iLiked && (
<LikeIcon
alt='Like'
src='/images/like-active-icon.svg'
/>
)}
{groupTitle && (
<Fragment>
{groupTitle in groupTitlesMap
? <Time><T9n t={groupTitlesMap[groupTitle]} /></Time>
: <Time>{startMinute}&apos;</Time>}
</Fragment>
)}
<Title>{getTitle()}</Title>
</Button>
)
}

@ -1,35 +0,0 @@
import styled from 'styled-components/macro'
import { Button as ButtonBase } from 'features/MatchSidePlaylists/styled'
type ButtonProps = {
groupTitle: string,
}
export const Button = styled(ButtonBase)<ButtonProps>`
justify-content: initial;
padding-left: ${({ groupTitle }) => (groupTitle ? 77 : 42)}px;
`
export const Title = styled.div`
font-size: 12px;
color: ${({ theme }) => theme.colors.white};
`
export const LikeIcon = styled.img`
position: absolute;
left: 20px;
width: 11px;
height: 11px;
`
export const Time = styled.span`
position: absolute;
right: calc(100% - 67px);
font-size: 12px;
color: ${({ theme }) => theme.colors.white};
`
export const LikesCount = styled.span`
font-weight: 600;
`

@ -1,45 +0,0 @@
import { T9n } from 'features/T9n'
import type { LikeEvent as LikeEventType } from 'features/MatchPage/store'
import {
BlockTitle,
Event,
TextEvent,
List,
} from '../TabEvents/styled'
import { LikeEvent } from '../LikeEvent'
type Props = {
groupTitle: string,
likeEvents: Array<LikeEventType>,
}
export const LikesList = ({
groupTitle,
likeEvents,
}: Props) => {
if (!likeEvents.length) return null
const title = groupTitle.startsWith('half') ? 'half_time' : groupTitle
return (
<List>
{title && (
<TextEvent>
<BlockTitle>
<T9n t={title} />
</BlockTitle>
</TextEvent>
)}
{likeEvents.map((likeEvent) => (
<Event key={likeEvent.episode}>
<LikeEvent
likeEvent={likeEvent}
groupTitle={title}
/>
</Event>
))}
</List>
)
}

@ -157,7 +157,7 @@ export const Tabs = styled(TabsBase)`
color: #ffff; color: #ffff;
flex: 1 1 auto; flex: 1 1 auto;
${isMobileDevice && 'padding-left: 33px;'} ${isMobileDevice ? 'padding-left: 33px;' : 'padding-left: 20px;'}
` `
type TTab = { type TTab = {

@ -1,235 +0,0 @@
/* eslint-disable sort-keys */
import { useEffect } from 'react'
import { useRecoilState, useRecoilValue } from 'recoil'
import findKey from 'lodash/findKey'
import orderBy from 'lodash/orderBy'
import isNumber from 'lodash/isNumber'
import { useVideoBounds } from 'hooks'
import type { MatchInfo } from 'requests'
import { T9n } from 'features/T9n'
import {
filterTypeState,
filteredLikesState,
sortTypeState,
type LikeEvent,
useMatchPageStore,
} from 'features/MatchPage/store'
import { Tabs } from 'features/MatchSidePlaylists/config'
import { LikesList } from '../LikesList'
import {
Wrapper,
Tabs as SortTabs,
Tab as SortTab,
ButtonsBlock,
HalfEvents,
HalfList,
} from '../TabEvents/styled'
import {
Tab,
TabList,
TabTitle,
} from './styled'
type Props = {
profile?: MatchInfo,
}
const groupTitles = [
'pre_match',
'1st_half',
'half1-2',
'half1-3',
'half1-4',
'half1-5',
'2nd_half',
'half2-3',
'half2-4',
'half2-5',
'3rd_half',
'half3-4',
'half3-5',
'4th_half',
'half4-5',
'5th_half',
'full_time',
]
export const TabLikes = ({ profile: matchProfile }:Props) => {
const [filterType, setFilterType] = useRecoilState(filterTypeState)
const [sortType, setSortType] = useRecoilState(sortTypeState)
const likeEvents = useRecoilValue(filteredLikesState)
const { selectedPlaylist, setEpisodeInfo } = useMatchPageStore()
const videoBounds = useVideoBounds()
const needUseGroups = videoBounds && videoBounds.findIndex(({ h }) => h === '1') !== -1
const getHalfInterval = (period: number) => {
const bound = videoBounds?.find(({ h }) => h === String(period))
return [bound?.s && Number(bound.s), bound?.e && Number(bound.e)]
}
const getTimeoutInterval = (firstPeriod: number, secondPeriod: number) => {
const firstBound = videoBounds?.find(({ h }) => h === String(firstPeriod))
const secondBound = videoBounds?.find(({ h }) => h === String(secondPeriod))
return [firstBound?.e && Number(firstBound.e) + 1, secondBound?.s && Number(secondBound.s) - 1]
}
const getGroupedLikes = () => {
if (!needUseGroups) return {}
const firstHalf = videoBounds?.find(({ h }) => h === '1')
const lastHalf = videoBounds?.[videoBounds.length - 1]
const groupedLikes: Record<string, Array<LikeEvent>> = {
pre_match: [],
'1st_half': [],
'half1-2': [],
'2nd_half': [],
'half2-3': [],
'3rd_half': [],
'half3-4': [],
'4th_half': [],
'half4-5': [],
'5th_half': [],
full_time: [],
'half1-3': [],
'half1-4': [],
'half1-5': [],
'half2-4': [],
'half2-5': [],
'half3-5': [],
}
const intervals: Record<string, Array<number | undefined | string>> = {
pre_match: [0, Number(firstHalf?.s || 0) - 1],
'1st_half': getHalfInterval(1),
'half1-2': getTimeoutInterval(1, 2),
'2nd_half': getHalfInterval(2),
'half2-3': getTimeoutInterval(2, 3),
'3rd_half': getHalfInterval(3),
'half3-4': getTimeoutInterval(3, 4),
'4th_half': getHalfInterval(4),
'half4-5': getTimeoutInterval(4, 5),
'5th_half': getHalfInterval(5),
full_time: [Number(lastHalf?.e || 0) + 1, 1e5],
'half3-5': getTimeoutInterval(3, 5),
'half2-4': getTimeoutInterval(2, 4),
'half2-5': getTimeoutInterval(2, 5),
'half1-3': getTimeoutInterval(1, 3),
'half1-4': getTimeoutInterval(1, 4),
'half1-5': getTimeoutInterval(1, 5),
}
likeEvents.forEach((likeEvent) => {
const half = findKey(intervals, (interval) => {
if (isNumber(interval[0]) && isNumber(interval[1])) {
return likeEvent.s >= interval[0] && likeEvent.s <= interval[1]
}
if (isNumber(interval[0])
&& !interval[1]
&& likeEvent.s >= interval[0]
&& matchProfile?.live
) return true
return false
})
half && groupedLikes[half].push(likeEvent)
})
Object.keys(groupedLikes).forEach((key) => {
groupedLikes[key] = orderBy(
groupedLikes[key],
's',
sortType,
)
})
return groupedLikes
}
const groupedLikes = getGroupedLikes()
const likesEntries = needUseGroups
? orderBy(
Object.entries(groupedLikes),
([title]) => groupTitles.findIndex((key) => key === title),
sortType,
)
: []
const sortedLikes = needUseGroups ? [] : orderBy(
likeEvents,
's',
sortType,
)
useEffect(() => {
selectedPlaylist.tab === Tabs.LIKES && setEpisodeInfo({})
}, [setEpisodeInfo, selectedPlaylist.tab])
return (
<Wrapper>
<ButtonsBlock>
<SortTabs>
<T9n t={sortType === 'asc' ? 'from_start_match' : 'from_end_match'} />
<SortTab
active={sortType === 'asc'}
onClick={() => setSortType('asc')}
id='match_likes_sort_start'
/>
<SortTab
active={sortType === 'desc'}
onClick={() => setSortType('desc')}
id='match_likes_sort_final'
/>
</SortTabs>
<TabList>
<Tab
aria-pressed={filterType === 'totalLikes'}
onClick={() => setFilterType('totalLikes')}
>
<TabTitle t='total_likes' />
</Tab>
<Tab
aria-pressed={filterType === 'myLikes'}
onClick={() => setFilterType('myLikes')}
>
<TabTitle t='my_likes' />
</Tab>
</TabList>
</ButtonsBlock>
{needUseGroups ? (
<HalfList>
{likesEntries.map(([groupTitle, likesInGroup]) => (
<HalfEvents key={groupTitle}>
<LikesList
groupTitle={groupTitle}
likeEvents={likesInGroup}
/>
</HalfEvents>
))}
</HalfList>
)
: (
<HalfList>
<HalfEvents>
<LikesList
groupTitle=''
likeEvents={sortedLikes}
/>
</HalfEvents>
</HalfList>
)}
</Wrapper>
)
}

@ -1,40 +0,0 @@
import styled, { css } from 'styled-components/macro'
import { isMobileDevice } from 'config'
import { T9n } from 'features/T9n'
export const TabList = styled.div.attrs({ role: 'tablist' })`
display: flex;
gap: 12px;
${isMobileDevice && css`
padding-right: 33px;
`}
`
export const TabTitle = styled(T9n)`
position: relative;
color: rgba(255, 255, 255, 0.5);
`
export const Tab = styled.button.attrs({ role: 'tab' })`
position: relative;
display: flex;
justify-content: center;
align-items: center;
padding: 0 0 5px;
font-size: 12px;
cursor: pointer;
border: none;
background: none;
border-bottom: 2px solid transparent;
&[aria-pressed="true"] {
border-color: ${({ theme }) => theme.colors.white};
${TabTitle} {
color: ${({ theme }) => theme.colors.white};
}
}
`

@ -3,5 +3,4 @@ export enum Tabs {
EVENTS, EVENTS,
STATS, STATS,
PLAYERS, PLAYERS,
LIKES,
} }

@ -27,7 +27,7 @@ export const useMatchSidePlaylists = () => {
const videoBounds = matchScore?.video_bounds || matchProfile?.video_bounds const videoBounds = matchScore?.video_bounds || matchProfile?.video_bounds
const matchStatus = matchScore?.c_match_calc_status || matchProfile?.c_match_calc_status const matchStatus = matchScore?.c_match_calc_status || matchProfile?.c_match_calc_status
const isMatchParsed = Number(matchStatus) > MatchStatuses.Upcoming const showTabs = Number(matchStatus) > MatchStatuses.Upcoming
&& findIndex(videoBounds, ({ h, s }) => h === '1' && !isNil(s)) !== -1 && findIndex(videoBounds, ({ h, s }) => h === '1' && !isNil(s)) !== -1
useEffect(() => { useEffect(() => {
@ -35,8 +35,8 @@ export const useMatchSidePlaylists = () => {
}, [selectedTab, closePopup]) }, [selectedTab, closePopup])
return { return {
isMatchParsed,
onTabClick: setSelectedTab, onTabClick: setSelectedTab,
selectedTab, selectedTab,
showTabs,
} }
} }

@ -4,12 +4,11 @@ import {
useState, useState,
} from 'react' } from 'react'
import { createPortal } from 'react-dom' import { createPortal } from 'react-dom'
import { useRecoilValue } from 'recoil'
import { useTour } from '@reactour/tour' import { useTour } from '@reactour/tour'
import type { PlaylistOption } from 'features/MatchPage/types' import type { PlaylistOption } from 'features/MatchPage/types'
import { totalLikesState, useMatchPageStore } from 'features/MatchPage/store' import { useMatchPageStore } from 'features/MatchPage/store'
import { import {
Spotlight, Spotlight,
Steps, Steps,
@ -31,7 +30,6 @@ import { TabEvents } from './components/TabEvents'
import { TabWatch } from './components/TabWatch' import { TabWatch } from './components/TabWatch'
import { TabPlayers } from './components/TabPlayers' import { TabPlayers } from './components/TabPlayers'
import { TabStats } from './components/TabStats' import { TabStats } from './components/TabStats'
import { TabLikes } from './components/TabLikes'
import { useMatchSidePlaylists } from './hooks' import { useMatchSidePlaylists } from './hooks'
import { import {
@ -42,6 +40,7 @@ import {
TabIcon, TabIcon,
TabTitle, TabTitle,
Container, Container,
TabButton,
EventsAdsWrapper, EventsAdsWrapper,
} from './styled' } from './styled'
import { HeaderAds } from '../../components/Ads' import { HeaderAds } from '../../components/Ads'
@ -51,7 +50,6 @@ const tabPanes = {
[Tabs.EVENTS]: TabEvents, [Tabs.EVENTS]: TabEvents,
[Tabs.STATS]: TabStats, [Tabs.STATS]: TabStats,
[Tabs.PLAYERS]: TabPlayers, [Tabs.PLAYERS]: TabPlayers,
[Tabs.LIKES]: TabLikes,
} }
type Props = { type Props = {
@ -63,8 +61,6 @@ export const MatchSidePlaylists = ({
onSelect, onSelect,
selectedPlaylist, selectedPlaylist,
}: Props) => { }: Props) => {
const likeEvents = useRecoilValue(totalLikesState)
const { const {
ads, ads,
hideProfileCard, hideProfileCard,
@ -77,8 +73,8 @@ export const MatchSidePlaylists = ({
} = useMatchPageStore() } = useMatchPageStore()
const { const {
isMatchParsed,
onTabClick, onTabClick,
showTabs,
} = useMatchSidePlaylists() } = useMatchSidePlaylists()
const { const {
@ -93,7 +89,6 @@ export const MatchSidePlaylists = ({
const containerRef = useRef<HTMLDivElement | null>(null) const containerRef = useRef<HTMLDivElement | null>(null)
const tabPaneContainerRef = useRef<HTMLDivElement | null>(null) const tabPaneContainerRef = useRef<HTMLDivElement | null>(null)
const tabsGroupRef = useRef<HTMLDivElement | null>(null)
const [hasTabPaneScroll, setTabPaneScroll] = useState(false) const [hasTabPaneScroll, setTabPaneScroll] = useState(false)
@ -118,14 +113,14 @@ export const MatchSidePlaylists = ({
if ( if (
getLocalStorageItem(TOUR_COMPLETED_STORAGE_KEY) === 'true' getLocalStorageItem(TOUR_COMPLETED_STORAGE_KEY) === 'true'
|| isOpen || isOpen
|| !isMatchParsed || !showTabs
|| Number(profile?.c_match_calc_status) < 2 || Number(profile?.c_match_calc_status) < 2
) return undefined ) return undefined
const timer = setTimeout(() => setIsOpen(true), 1500) const timer = setTimeout(() => setIsOpen(true), 1500)
return () => clearTimeout(timer) return () => clearTimeout(timer)
}, [isMatchParsed, setIsOpen, profile?.c_match_calc_status, isOpen]) }, [showTabs, setIsOpen, profile?.c_match_calc_status, isOpen])
useEventListener({ useEventListener({
callback: () => { callback: () => {
@ -142,19 +137,6 @@ export const MatchSidePlaylists = ({
target: containerRef, target: containerRef,
}) })
const getTabsSpace = () => {
const tabsGroup = tabsGroupRef.current
const tabsWidth = isMobileDevice && containerRef.current
? containerRef.current.clientWidth - 20
: 306
return tabsGroup?.children.length && tabsGroup.children.length > 1
? (tabsWidth - (Array.from(tabsGroup.children) as Array<HTMLButtonElement>)
.reduce((acc, elem) => acc + elem.clientWidth, 0)) / tabsGroup.children.length - 1
: 0
}
return ( return (
<Wrapper <Wrapper
ref={containerRef} ref={containerRef}
@ -163,6 +145,8 @@ export const MatchSidePlaylists = ({
isTourOpen={Boolean(isOpen)} isTourOpen={Boolean(isOpen)}
isHidden={!profileCardShown} isHidden={!profileCardShown}
> >
{showTabs
&& (
<TabsWrapper> <TabsWrapper>
{selectedTab === Tabs.EVENTS {selectedTab === Tabs.EVENTS
&& ads && ads
@ -171,51 +155,37 @@ export const MatchSidePlaylists = ({
<HeaderAds ads={ads.filter(({ position }) => position.id === adsPositionId)} /> <HeaderAds ads={ads.filter(({ position }) => position.id === adsPositionId)} />
</EventsAdsWrapper> </EventsAdsWrapper>
)} )}
<TabsGroup <TabsGroup>
ref={tabsGroupRef}
space={getTabsSpace()}
>
{(isMatchParsed || likeEvents.length > 0) && (
<Tab <Tab
aria-pressed={selectedTab === Tabs.WATCH} aria-pressed={selectedTab === Tabs.WATCH}
onClick={() => onTabClick(Tabs.WATCH)} onClick={() => onTabClick(Tabs.WATCH)}
id='match_watch' id='match_watch'
> >
<TabButton>
<TabIcon icon='watch' /> <TabIcon icon='watch' />
<TabTitle t='watch' /> <TabTitle t='watch' />
</TabButton>
</Tab> </Tab>
)}
{isMatchParsed && (
<Tab <Tab
aria-pressed={selectedTab === Tabs.EVENTS} aria-pressed={selectedTab === Tabs.EVENTS}
onClick={() => onTabClick(Tabs.EVENTS)} onClick={() => onTabClick(Tabs.EVENTS)}
id='match_plays' id='match_plays'
> >
<TabButton>
<TabIcon icon='plays' /> <TabIcon icon='plays' />
<TabTitle t='actions' /> <TabTitle t='actions' />
</TabButton>
</Tab> </Tab>
)}
{likeEvents.length > 0 && (
<Tab
aria-pressed={selectedTab === Tabs.LIKES}
onClick={() => onTabClick(Tabs.LIKES)}
id='match_likes'
>
<TabIcon icon='likes' />
<TabTitle t='likes' />
</Tab>
)}
{isMatchParsed && (
<Tab <Tab
aria-pressed={selectedTab === Tabs.PLAYERS} aria-pressed={selectedTab === Tabs.PLAYERS}
onClick={() => onTabClick(Tabs.PLAYERS)} onClick={() => onTabClick(Tabs.PLAYERS)}
id='match_players' id='match_players'
> >
<TabButton>
<TabIcon icon='players' /> <TabIcon icon='players' />
<TabTitle t='players' /> <TabTitle t='players' />
</TabButton>
</Tab> </Tab>
)}
{isMatchParsed && (
<Tab <Tab
aria-pressed={selectedTab === Tabs.STATS} aria-pressed={selectedTab === Tabs.STATS}
onClick={() => onTabClick(Tabs.STATS)} onClick={() => onTabClick(Tabs.STATS)}
@ -225,12 +195,14 @@ export const MatchSidePlaylists = ({
{Boolean(currentStep === Steps.Start && isOpen) && ( {Boolean(currentStep === Steps.Start && isOpen) && (
<Spotlight /> <Spotlight />
)} )}
<TabButton>
<TabIcon icon='stats' /> <TabIcon icon='stats' />
<TabTitle t='stats' /> <TabTitle t='stats' />
</TabButton>
</Tab> </Tab>
)}
</TabsGroup> </TabsGroup>
</TabsWrapper> </TabsWrapper>
)}
<Container <Container
hasScroll={hasTabPaneScroll} hasScroll={hasTabPaneScroll}
ref={tabPaneContainerRef} ref={tabPaneContainerRef}

@ -53,14 +53,10 @@ export const Wrapper = styled.div<WrapperProps>`
export const TabsWrapper = styled.div`` export const TabsWrapper = styled.div``
type TabsGroupProps = { export const TabsGroup = styled.div.attrs({ role: 'tablist' })`
space: number,
}
export const TabsGroup = styled.div.attrs({ role: 'tablist' })<TabsGroupProps>`
display: flex; display: flex;
justify-content: center; justify-content: center;
gap: min(${({ space }) => space}px, ${isMobileDevice ? 19 : 16}px); gap: ${isMobileDevice ? 30 : 20}px;
padding-top: 10px; padding-top: 10px;
` `
@ -71,8 +67,7 @@ export const TabTitle = styled(T9n)`
color: ${({ theme }) => theme.colors.white}; color: ${({ theme }) => theme.colors.white};
` `
export const Tab = styled.button.attrs({ role: 'tab' })` export const TabButton = styled.button`
position: relative;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: space-between; justify-content: space-between;
@ -83,9 +78,19 @@ export const Tab = styled.button.attrs({ role: 'tab' })`
cursor: pointer; cursor: pointer;
border: none; border: none;
background: none; background: none;
`
export const Tab = styled.div.attrs({ role: 'tab' })`
position: relative;
&[aria-pressed="true"], :hover { &[aria-pressed="true"], :hover {
${TabButton} {
opacity: 1; opacity: 1;
${TabTitle} {
font-weight: 600;
}
}
} }
:only-child { :only-child {
@ -94,7 +99,7 @@ export const Tab = styled.button.attrs({ role: 'tab' })`
` `
type TabIconProps = { type TabIconProps = {
icon: 'watch' | 'plays' | 'players' | 'stats' | 'likes', icon: 'watch' | 'plays' | 'players' | 'stats',
} }
export const TabIcon = styled.div<TabIconProps>` export const TabIcon = styled.div<TabIconProps>`
@ -107,7 +112,7 @@ export const TabIcon = styled.div<TabIconProps>`
background-position: center; background-position: center;
background-size: contain; background-size: contain;
${({ icon }) => (['likes', 'players'].includes(icon) ${({ icon }) => (icon === 'players'
? css` ? css`
background-size: 25px; background-size: 25px;
` `

@ -2,7 +2,7 @@ import { Fragment } from 'react'
import isEmpty from 'lodash/isEmpty' import isEmpty from 'lodash/isEmpty'
import type { Match } from 'helpers' import type { Match } from 'features/Matches/hooks'
import { MatchesSlider } from 'features/MatchesSlider' import { MatchesSlider } from 'features/MatchesSlider'
import { MatchesGrid } from 'features/MatchesGrid' import { MatchesGrid } from 'features/MatchesGrid'
import { TournamentList } from 'features/TournamentList' import { TournamentList } from 'features/TournamentList'

@ -1,8 +1,8 @@
import map from 'lodash/map' import map from 'lodash/map'
import type { MatchesBySection, MatchesDto } from 'requests' import type { MatchesBySection, Matches } from 'requests'
const addSportTypeToMatches = (matches: MatchesDto, sport: number) => ( const addSportTypeToMatches = (matches: Matches, sport: number) => (
map(matches, (match) => ({ ...match, sport })) map(matches, (match) => ({ ...match, sport }))
) )

@ -1,4 +1,4 @@
import type { MatchDto } from 'requests' import type { Match } from 'requests'
import { getMatchAccess, MatchAccess } from '..' import { getMatchAccess, MatchAccess } from '..'
@ -14,7 +14,7 @@ type Args = {
const createMatch = (args: Args) => ({ const createMatch = (args: Args) => ({
...args, ...args,
} as MatchDto) } as Match)
const user = undefined const user = undefined

@ -1,4 +1,4 @@
import type { MatchDto } from 'requests' import type { Match } from 'requests'
import type { User } from 'oidc-client' import type { User } from 'oidc-client'
import { isFuture } from 'date-fns' import { isFuture } from 'date-fns'
@ -13,7 +13,7 @@ export enum MatchAccess {
ViewMatchPopupWithoutUser = 'ViewMatchPopupWithoutUser', ViewMatchPopupWithoutUser = 'ViewMatchPopupWithoutUser',
} }
export const getMatchAccess = (match: MatchDto, user: User | undefined) => { export const getMatchAccess = (match: Match, user: User | undefined) => {
const { const {
access, access,
date, date,

@ -1,19 +1,16 @@
import map from 'lodash/map' import map from 'lodash/map'
import orderBy from 'lodash/orderBy' import orderBy from 'lodash/orderBy'
import format from 'date-fns/format' import format from 'date-fns/format'
import type { User } from 'oidc-client' import type { User } from 'oidc-client'
import type { MatchDto } from 'requests' import type { Match } from 'requests'
import { parseDate } from 'helpers/parseDate' import { parseDate } from 'helpers/parseDate'
import { getMatchAccess } from 'features/Matches/helpers/getMatchClickAction' import { getMatchAccess } from './getMatchClickAction'
export type Match = ReturnType<typeof prepareMatch>
const prepareMatch = (match: MatchDto, user?: User | undefined) => { const prepareMatch = (match: Match, user?: User | undefined) => {
const { const {
calc, calc,
country, country,
@ -59,22 +56,14 @@ const prepareMatch = (match: MatchDto, user?: User | undefined) => {
} }
} }
export const prepareMatches = ( export const prepareMatches = (matches: Array<Match>, user?: User | undefined) => {
matches: Array<MatchDto>,
user?: User | undefined,
liveOrder: boolean = true,
): Array<Match> => {
const preparedMatches = map( const preparedMatches = map(
matches, matches,
(match) => prepareMatch(match, user), (match) => prepareMatch(match, user),
) )
if (liveOrder) {
return orderBy( return orderBy(
preparedMatches, preparedMatches,
['live'], ['live'],
['desc'], ['desc'],
) )
} }
return preparedMatches
}

@ -13,10 +13,12 @@ import { useRequest } from 'hooks'
import { usePreferencesStore } from 'features/PreferencesPopup' import { usePreferencesStore } from 'features/PreferencesPopup'
import { isMobileDevice } from 'config/userAgent' import { isMobileDevice } from 'config/userAgent'
import { prepareMatches } from 'helpers' import { prepareMatches } from './helpers/prepareMatches'
import { useAuthStore } from '../AuthStore' import { useAuthStore } from '../AuthStore'
export type Match = ReturnType<typeof prepareMatches>[number]
export type Props = { export type Props = {
fetch: (limit: number, offset: number) => Promise<MatchesBySection>, fetch: (limit: number, offset: number) => Promise<MatchesBySection>,
} }

@ -14,6 +14,8 @@ import { useMatches } from './hooks'
import { MatchesList } from './components/MatchesList' import { MatchesList } from './components/MatchesList'
import { Loading } from './styled' import { Loading } from './styled'
export type { Match } from './hooks'
export const Matches = memo((props: Props) => { export const Matches = memo((props: Props) => {
const { const {
fetchMoreMatches, fetchMoreMatches,

@ -11,10 +11,10 @@ import { getLiveScores } from 'requests'
import { MatchCard } from 'features/MatchCard' import { MatchCard } from 'features/MatchCard'
import { TournamentList } from 'features/TournamentList' import { TournamentList } from 'features/TournamentList'
import type { Match } from 'features/Matches'
import { useHeaderFiltersStore } from 'features/HeaderFilters' import { useHeaderFiltersStore } from 'features/HeaderFilters'
import { readToken } from 'helpers' import { readToken } from 'helpers'
import type { Match } from 'helpers'
import { useMatchSwitchesStore } from '../MatchSwitches' import { useMatchSwitchesStore } from '../MatchSwitches'
import { useHomePage } from '../HomePage/hooks' import { useHomePage } from '../HomePage/hooks'
@ -41,7 +41,6 @@ export const MatchesGrid = memo(({ matches }: MatchesGridProps) => {
selectedDate, selectedDate,
selectedFilters, selectedFilters,
selectedLeague, selectedLeague,
selectedMode,
selectedSport, selectedSport,
updateSportIds, updateSportIds,
} = useHeaderFiltersStore() } = useHeaderFiltersStore()
@ -61,7 +60,7 @@ export const MatchesGrid = memo(({ matches }: MatchesGridProps) => {
} }
if (isHomePage && selectedSport) { if (isHomePage && selectedSport) {
return matches.filter((match) => compareSport(match.sportType, selectedSport)) return matches.filter((match) => compareSport(match, selectedSport))
} }
return matches return matches
@ -84,7 +83,7 @@ export const MatchesGrid = memo(({ matches }: MatchesGridProps) => {
updateSportIds(matches) updateSportIds(matches)
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedDate, matches, selectedMode]) }, [selectedDate, matches])
return ( return (
<Wrapper> <Wrapper>

@ -9,33 +9,28 @@ import {
import throttle from 'lodash/throttle' import throttle from 'lodash/throttle'
import type { Match } from 'helpers' import type { Match } from 'features/Matches'
import { MATCH_CARD_WIDTH, MATCH_CARD_GAP } from 'features/MatchCard/config'
const MATCHES_TO_SCROLL = 3 const MATCHES_TO_SCROLL = 6
const SCROLLING_DELAY = 350 const SCROLLING_DELAY = 750
export const useMatchesSlider = (matches: Array<Match>) => { export const useMatchesSlider = (matches: Array<Match>) => {
const slidesRef = useRef<HTMLUListElement>(null) const slidesRef = useRef<HTMLUListElement>(null)
const [showLeftArrow, setShowLeftArrow] = useState(false) const [showLeftArrow, setShowLeftArrow] = useState(false)
const [showRightArrow, setShowRigthArrow] = useState(false) const [showRightArrow, setShowRigthArrow] = useState(false)
const [isLeftArrowDisabled, setIsLeftArrowDisabled] = useState(false)
const [isRightArrowDisabled, setIsRightArrowDisabled] = useState(false)
const [sliderLiWidth, setSliderLiWidth] = useState(0)
const [sliderLiGap, setSliderLiGap] = useState(0)
useEffect(() => { useEffect(() => {
const { const {
// clientWidth = 0, clientWidth = 0,
scrollLeft = 0, scrollLeft = 0,
// scrollWidth = 0, scrollWidth = 0,
} = slidesRef.current || {} } = slidesRef.current || {}
// const scrollRight = scrollWidth - (scrollLeft + clientWidth) const scrollRight = scrollWidth - (scrollLeft + clientWidth)
// setShowRigthArrow(scrollRight > 1) setShowRigthArrow(scrollRight > 1)
// setShowLeftArrow(scrollRight > 1)
setIsLeftArrowDisabled(scrollLeft <= 0)
}, [matches, slidesRef]) }, [matches, slidesRef])
const onScroll = useCallback((e: SyntheticEvent<HTMLUListElement>) => { const onScroll = useCallback((e: SyntheticEvent<HTMLUListElement>) => {
@ -44,65 +39,21 @@ export const useMatchesSlider = (matches: Array<Match>) => {
scrollLeft: targetScrollLeft, scrollLeft: targetScrollLeft,
scrollWidth: targetScrollWidth, scrollWidth: targetScrollWidth,
} = e.currentTarget } = e.currentTarget
const targetScrollRight = targetScrollWidth - (targetScrollLeft + targetClientWidth)
// setShowLeftArrow(targetScrollLeft > 1)
// setShowRigthArrow(targetScrollRight > 1)
setIsLeftArrowDisabled(targetScrollLeft === 0)
setIsRightArrowDisabled(Math.round(targetScrollRight) <= 1)
}, [])
const onWrapperMouseEnter = () => {
const {
clientWidth = 0,
scrollLeft = 0,
scrollWidth = 0,
} = slidesRef.current || {}
const scrollRight = scrollWidth - (scrollLeft + clientWidth)
if (scrollRight > 1 || scrollLeft > 1) {
setShowLeftArrow(true)
setShowRigthArrow(true)
}
}
const onWrapperMouseLeave = () => { setShowLeftArrow(targetScrollLeft > 1)
setShowLeftArrow(false) setShowRigthArrow((targetScrollWidth - (targetScrollLeft + targetClientWidth)) > 1)
setShowRigthArrow(false)
}
useEffect(() => {
const liFirst = slidesRef.current?.querySelectorAll('li')[0]?.getBoundingClientRect()
const liWidth = liFirst?.width
const liFirstLeft = liFirst?.left
const liSecondLeft = slidesRef.current?.querySelectorAll('li')[1]?.getBoundingClientRect().left
if (liWidth && liFirstLeft && liSecondLeft) {
setSliderLiWidth(liWidth)
setSliderLiGap(liSecondLeft - liFirstLeft - liWidth)
}
}, []) }, [])
const slideLeft = useMemo(() => throttle(() => { const slideLeft = useMemo(() => throttle(() => {
slidesRef.current!.scrollBy({ slidesRef.current!.scrollBy(-((MATCH_CARD_WIDTH + MATCH_CARD_GAP) * MATCHES_TO_SCROLL), 0)
behavior: 'smooth', }, SCROLLING_DELAY), [])
left: -((sliderLiWidth + sliderLiGap) * MATCHES_TO_SCROLL),
top: 0,
})
}, SCROLLING_DELAY), [sliderLiGap, sliderLiWidth])
const slideRight = useMemo(() => throttle(() => { const slideRight = useMemo(() => throttle(() => {
slidesRef.current!.scrollBy({ slidesRef.current!.scrollBy((MATCH_CARD_WIDTH + MATCH_CARD_GAP) * MATCHES_TO_SCROLL, 0)
behavior: 'smooth', }, SCROLLING_DELAY), [])
left: ((sliderLiWidth + sliderLiGap) * MATCHES_TO_SCROLL),
top: 0,
})
}, SCROLLING_DELAY), [sliderLiGap, sliderLiWidth])
return { return {
isLeftArrowDisabled,
isRightArrowDisabled,
onScroll, onScroll,
onWrapperMouseEnter,
onWrapperMouseLeave,
showLeftArrow, showLeftArrow,
showRightArrow, showRightArrow,
slideLeft, slideLeft,

@ -1,10 +1,7 @@
import { useEffect } from 'react'
import map from 'lodash/map' import map from 'lodash/map'
import isEmpty from 'lodash/isEmpty' import isEmpty from 'lodash/isEmpty'
import type { Match } from 'helpers' import type { Match } from 'features/Matches'
import { MatchCard } from 'features/MatchCard' import { MatchCard } from 'features/MatchCard'
import { useMatchesSlider } from './hooks' import { useMatchesSlider } from './hooks'
@ -12,23 +9,15 @@ import {
Wrapper, Wrapper,
Arrow, Arrow,
Slides, Slides,
ArrowButton,
} from './styled' } from './styled'
type MatchesSliderProps = { type MatchesSliderProps = {
cardSize?: number,
matches: Array<Match>, matches: Array<Match>,
} }
const PADDING_PARENT = 10 export const MatchesSlider = ({ matches }: MatchesSliderProps) => {
export const MatchesSlider = ({ cardSize, matches }: MatchesSliderProps) => {
const { const {
isLeftArrowDisabled,
isRightArrowDisabled,
onScroll, onScroll,
onWrapperMouseEnter,
onWrapperMouseLeave,
showLeftArrow, showLeftArrow,
showRightArrow, showRightArrow,
slideLeft, slideLeft,
@ -36,91 +25,26 @@ export const MatchesSlider = ({ cardSize, matches }: MatchesSliderProps) => {
slidesRef, slidesRef,
} = useMatchesSlider(matches) } = useMatchesSlider(matches)
const scrollToMatchByIndex = (index: number) => {
const match = slidesRef.current!.querySelectorAll('li')[index]
const offsetLeftCount = match.offsetLeft
const offsetLeft = offsetLeftCount - PADDING_PARENT
setTimeout(() => {
slidesRef.current!.scrollBy({
// @ts-ignore
behavior: 'instant',
left: offsetLeft,
})
}, 0)
}
// скролл к лайв матчам или сегодняшней дате
useEffect(() => {
const matchIndexLive = matches.findIndex(({ live }) => live)
if (matchIndexLive !== -1) {
scrollToMatchByIndex(matchIndexLive)
return
}
const matchIndex = matches.findIndex((item) => new Date() <= item.date)
if (matchIndex !== -1) {
scrollToMatchByIndex(matchIndex)
return
}
const slidesRefClientWidth = slidesRef.current!.clientWidth
const slidesRefScrollWidth = slidesRef.current!.scrollWidth
setTimeout(() => {
slidesRef.current!.scrollBy({
// @ts-ignore
behavior: 'instant',
left: slidesRefScrollWidth - slidesRefClientWidth + PADDING_PARENT,
})
}, 0)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
if (isEmpty(matches)) return null if (isEmpty(matches)) return null
return ( return (
<Wrapper <Wrapper>
onMouseEnter={onWrapperMouseEnter}
onMouseLeave={onWrapperMouseLeave}
>
{showLeftArrow && ( {showLeftArrow && (
<ArrowButton
direction='left'
onClick={slideLeft}
disabled={isLeftArrowDisabled}
>
<Arrow <Arrow
disabled={isLeftArrowDisabled} type='arrowLeft'
direction='left' aria-label='Slide left'
onClick={slideLeft}
/> />
</ArrowButton>
)} )}
<Slides <Slides ref={slidesRef} onScroll={onScroll}>
ref={slidesRef} {map(matches, (match) => <MatchCard match={match} key={match.id} />)}
onScroll={onScroll}
size={cardSize}
>
{map(matches, (match) => (
<MatchCard
match={match}
key={match.id}
/>
))}
</Slides> </Slides>
{showRightArrow && ( {showRightArrow && (
<ArrowButton
direction='right'
onClick={slideRight}
disabled={isRightArrowDisabled}
>
<Arrow <Arrow
disabled={isRightArrowDisabled} type='arrowRight'
direction='right' aria-label='Slide right'
onClick={slideRight}
/> />
</ArrowButton>
)} )}
</Wrapper> </Wrapper>
) )

@ -1,94 +1,53 @@
import styled, { css } from 'styled-components/macro' import styled from 'styled-components/macro'
import { CardWrapper, CardWrapperOuter } from '../MatchCard/styled' import { devices } from 'config/devices'
import { CardWrapper } from '../MatchCard/styled'
export const Wrapper = styled.div` export const Wrapper = styled.div`
position: relative; position: relative;
overflow: hidden; margin-bottom: 16px;
padding-right: 5px;
` `
export const Slides = styled.ul<{ export const Slides = styled.ul`
size?: number,
}>`
display: flex; display: flex;
scroll-behavior: smooth; scroll-behavior: smooth;
overflow-x: auto; overflow-x: auto;
gap: 0.9rem;
padding: 10px 10px 10px 7px;
&::-webkit-scrollbar { &::-webkit-scrollbar {
display: none; display: none;
} }
scrollbar-width: none;
${CardWrapperOuter} {
padding-top: 0;
}
${CardWrapper} { ${CardWrapper} {
position: relative; width: 283px;
height: ${({ size }) => (size ? `${size}px` : '12.9rem')};
width: ${({ size }) => (size ? `${size}px` : '12.9rem')};
transition: scale 0.2s;
&:hover { @media ${devices.laptop} {
scale: 1.04; min-width: auto;
} width: 279px;
} }
`
type ArrowProps = {
direction: 'left' | 'right',
disabled?: boolean,
} }
export const Arrow = styled.span<ArrowProps>` @media ${devices.mobile} {
width: 1rem; flex-direction: column;
height: 1rem;
position: absolute;
border-left: 0.25rem solid #fff;
border-bottom: 0.25rem solid #fff;
top: 50%;
left: 50%;
border-radius: 3px;
${({ direction }) => (
direction === 'left'
? 'transform: translate(-50%, -50%) rotate(45deg);'
: 'transform: translate(-50%, -50%) rotate(225deg);'
)}
${({ disabled }) => (disabled ? css` ${CardWrapper} {
border-left: 0.25rem solid gray; width: 100%;
border-bottom: 0.25rem solid gray; }
` : '')} }
` `
export const ArrowButton = styled.button<ArrowProps>` export const Arrow = styled.div<{ type: 'arrowLeft' | 'arrowRight' }>`
border: none;
outline: none;
padding: 0;
background-color: transparent;
cursor: pointer;
position: absolute; position: absolute;
width: 2.28rem;
height: 2.28rem;
z-index: 3;
top: 50%; top: 50%;
left: ${({ type }) => (type === 'arrowLeft' ? '10px' : 'calc(100% - 10px)')};
width: 40px;
height: 40px;
background-position: center;
background-repeat: no-repeat;
background-image: url(${({ type }) => (type === 'arrowLeft'
? '/images/slideLeft.svg'
: '/images/slideRight.svg')});
cursor: pointer;
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
left: ${({ direction }) => (direction === 'left' ? '1.6rem' : 'calc(100% - 2.3rem)')}; z-index: 1;
&:hover {
${({ direction, disabled }) => (!disabled ? css`
width: 3rem;
height: 3rem;
left: ${direction === 'left' ? '2rem' : 'calc(100% - 2.8rem)'};
${Arrow} {
width: 1.5rem;
height: 1.5rem;
}
` : '')}
}
` `

@ -1,168 +0,0 @@
/* eslint-disable no-param-reassign */
import {
useCallback,
useEffect,
useMemo,
useState,
} from 'react'
import { useQuery } from 'react-query'
import { querieKeys, PAGES } from 'config'
import { useRouteMatch } from 'react-router-dom'
import {
MatchDto,
MatchesTimeline,
TimelineTournamentDto,
getLiveScores,
getTimelineMatches,
} from 'requests'
import { Match, prepareMatches } from 'helpers'
import { useAuthStore } from 'features/AuthStore'
import { useHeaderFiltersStore } from 'features/HeaderFilters'
import { useMatchSwitchesStore } from 'features/MatchSwitches'
export type TimelineTournamentList = Array<Omit<TimelineTournamentDto, 'matches'> & {
matches: Array<Match>,
}>
export const useTimeline = () => {
const isHomePage = useRouteMatch(PAGES.home)?.isExact
const { user } = useAuthStore()
const {
compareSport,
selectedMode,
selectedSport,
setSportIds,
} = useHeaderFiltersStore()
const { isScoreHidden } = useMatchSwitchesStore()
const [isTimelineFetching, setIsTimelineFetching] = useState(true)
const [onlineUpcomingMatches, setOnlineUpcomingMatches] = useState<Array<Match>>([])
const [tournamentList, setTournamentList] = useState<TimelineTournamentList>([])
const [timeline, setTimeline] = useState<MatchesTimeline>()
const prepareMatchesDto = useCallback((matches: Array<MatchDto>) => prepareMatches(
matches,
user,
false,
), [user])
useEffect(() => {
(async () => {
setIsTimelineFetching(true)
try {
const timelineFetched = await getTimelineMatches()
setTimeline(timelineFetched)
const convertedMatches = timelineFetched.online_upcoming[0].matches
const preparedMatches = prepareMatchesDto(convertedMatches)
setOnlineUpcomingMatches(preparedMatches)
setTournamentList([
...timelineFetched.favorite.map((item) => ({
...item,
matches: prepareMatchesDto(item.matches),
})),
...timelineFetched.promo.map((item) => ({
...item,
matches: prepareMatchesDto(item.matches),
})),
...timelineFetched.others.map((item) => ({
...item,
matches: prepareMatchesDto(item.matches),
})),
])
} catch (error) { /* empty */ } finally {
setIsTimelineFetching(false)
}
})()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
useQuery({
queryFn: async () => {
let isLiveMatch = false
if (timeline) {
for (const [, value] of Object.entries(timeline)) {
if (isLiveMatch) break
// eslint-disable-next-line no-loop-func
value.forEach((t) => {
const liveMatch = t.matches.find((match) => match.live)
if (liveMatch) {
isLiveMatch = true
}
})
}
}
if (!isScoreHidden && isLiveMatch) {
const scores = await getLiveScores()
tournamentList.forEach((tournament) => {
tournament.matches.forEach((match) => {
const score = scores.find((s) => (
s.match_id === match.id && s.sport_id === match.sportType
))
if (score) {
match.team1.score = score?.team1.score ?? match.team1.score
match.team1.penalty_score = score.team1.penalty_score ?? match.team1.penalty_score
match.team2.score = score?.team2.score ?? match.team2.score
match.team2.penalty_score = score.team2.penalty_score ?? match.team2.penalty_score
}
})
})
onlineUpcomingMatches.forEach((upcomingMatch) => {
const score = scores.find((s) => (
s.match_id === upcomingMatch.id && s.sport_id === upcomingMatch.sportType
))
if (score) {
upcomingMatch.team1.score = score?.team1.score ?? upcomingMatch.team1.score
upcomingMatch.team1.penalty_score = score.team1.penalty_score
?? upcomingMatch.team1.penalty_score
upcomingMatch.team2.score = score?.team2.score ?? upcomingMatch.team2.score
upcomingMatch.team2.penalty_score = score.team2.penalty_score
?? upcomingMatch.team2.penalty_score
}
})
}
},
queryKey: querieKeys.liveMatchScores,
refetchInterval: 30000,
})
const filteredTournamentsBySport = useMemo(() => {
if (isHomePage && selectedSport) {
return tournamentList.filter((t) => compareSport(t.sport_id, selectedSport))
}
return tournamentList
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [tournamentList, selectedSport])
const filteredOnlineUpcomingBySport = useMemo(() => {
if (isHomePage && selectedSport) {
return onlineUpcomingMatches.filter((m) => compareSport(m.sportType, selectedSport))
}
return onlineUpcomingMatches
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [onlineUpcomingMatches, selectedSport])
useEffect(() => {
if (!isHomePage) return
const sportIds = Array.from(new Set(tournamentList.map((t) => t.sport_id)))
setSportIds(sportIds)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [tournamentList, selectedMode])
return {
isTimelineFetching,
onlineUpcomingMatches: filteredOnlineUpcomingBySport,
tournamentList: filteredTournamentsBySport,
}
}

@ -1,151 +0,0 @@
import {
useCallback,
useEffect,
useLayoutEffect,
useRef,
useState,
} from 'react'
import isEmpty from 'lodash/isEmpty'
import { MatchesSlider } from 'features/MatchesSlider'
import { TimelineTournamentList, useTimeline } from 'features/MatchesTimeline/hooks'
import { InfiniteScroll } from 'features/InfiniteScroll'
import { T9n } from 'features/T9n'
import { TournamentSubtitle } from 'features/TournamentSubtitle'
import { ProfileTypes } from 'config'
import {
Content,
Tournament,
RowWrapper,
Wrapper,
TournamentLogo,
Loading,
TournamentSubtitleWrapper,
} from './styled'
const TOURNAMENT_LIMIT = 10
const MATCH_COUNT = 6
const PADDING = 30
const GAP_COUNT = 5
const SLIDER_ITEM_GAP = 0.9 // rem
export const MatchesTimeline = () => {
const {
isTimelineFetching,
onlineUpcomingMatches,
tournamentList,
} = useTimeline()
const [tournaments, setTournaments] = useState<TimelineTournamentList>([])
const pageRef = useRef(0)
const isLastPageRef = useRef(false)
const wrapperRef = useRef<HTMLDivElement>(null)
const [cardSize, setCardSize] = useState(0)
// вычисляем размеры плитки
useLayoutEffect(() => {
const offsetWidth = wrapperRef.current?.offsetWidth
const ulItemGapFromRem = SLIDER_ITEM_GAP * parseFloat(
getComputedStyle(document.documentElement).fontSize,
)
if (offsetWidth) {
const size = Math.round(
((offsetWidth - (ulItemGapFromRem * GAP_COUNT) - PADDING) / MATCH_COUNT),
)
setCardSize(size)
}
}, [])
const getTournaments = useCallback(() => tournamentList.slice(
pageRef.current * TOURNAMENT_LIMIT,
pageRef.current * TOURNAMENT_LIMIT + TOURNAMENT_LIMIT,
), [tournamentList])
const getMoreTournaments = () => {
if (isLastPageRef.current) return
const res = getTournaments()
if (res.length) {
setTournaments((prev) => ([
...prev,
...res,
]))
pageRef.current++
} else {
isLastPageRef.current = true
}
}
useEffect(() => {
if (tournamentList.length) {
pageRef.current = 0
isLastPageRef.current = false
tournamentList.length > 0 && setTournaments(getTournaments())
pageRef.current = 1
}
}, [getTournaments, tournamentList])
return (
<InfiniteScroll fullPageScroll onFetchMore={getMoreTournaments}>
<Wrapper ref={wrapperRef}>
{isTimelineFetching && <Loading><T9n t='loading' />...</Loading>}
{(!isEmpty(onlineUpcomingMatches)) && (
<RowWrapper>
<Content>
<Tournament
size={cardSize}
isOnlineUpcoming
>
LIVE & UPCOMING
</Tournament>
<MatchesSlider
cardSize={cardSize}
matches={onlineUpcomingMatches}
/>
</Content>
</RowWrapper>
)}
{tournaments.map(({
matches,
sport_id,
tournament,
tournament_id,
}) => (
<RowWrapper key={`${tournament_id}_${sport_id}`}>
<TournamentSubtitleWrapper>
<TournamentSubtitle
sportInfo={matches[0].sportInfo}
countryId={matches[0].countryId}
sportType={sport_id}
tournament={tournament}
/>
</TournamentSubtitleWrapper>
<Content>
<Tournament
size={cardSize}
gradientColor={tournament.color}
>
<TournamentLogo
id={tournament_id}
profileType={ProfileTypes.TOURNAMENTS}
sportType={sport_id}
/>
</Tournament>
<MatchesSlider
cardSize={cardSize}
matches={matches}
/>
</Content>
</RowWrapper>
))}
</Wrapper>
</InfiniteScroll>
)
}

@ -1,72 +0,0 @@
import { ProfileLogo } from 'features/ProfileLogo'
import { StyledLink } from 'features/TournamentSubtitle/styled'
import styled, { css } from 'styled-components/macro'
export const Wrapper = styled.div`
& > * {
margin-bottom: 20px;
}
`
export const RowWrapper = styled.div``
export const Content = styled.div`
display: flex;
`
export const Tournament = styled.div<{
gradientColor?: string,
isOnlineUpcoming?: boolean,
size?: number,
}>`
${({ gradientColor }) => (gradientColor
? css`background: linear-gradient(187deg, ${gradientColor} -4.49%, #000000 68.29%), #000000;`
: css`background-color: ${({ theme }) => theme.colors.matchCardBackground};`)}
// в будущем от этого нужно будет избавиться
${({ isOnlineUpcoming }) => isOnlineUpcoming && css`
background: linear-gradient(270deg, #C00 0%, #6A2131 100%);
`}
position: relative;
height: ${({ size }) => (size ? `${size}px` : '12.9rem')};
width: ${({ size }) => (size ? `${size}px` : '12.9rem')};
flex-shrink: 0;
margin: 10px 0.93rem 10px 0px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 3px;
font-size: 1.9rem;
font-weight: 700;
color: #FFFFFF;
text-align: start;
padding: 10px;
`
export const TournamentLogo = styled(ProfileLogo)`
position: absolute;
padding: 20px;
width: 100%;
height: 100%;
object-fit: scale-down;
`
export const Loading = styled.div`
height: 30px;
margin-top: 20px;
font-size: 24px;
color: #fff;
text-align: center;
`
export const TournamentSubtitleWrapper = styled.div`
margin-bottom: 10px;
${StyledLink} {
font-weight: 700;
color: #FFFFFF;
font-size: 1rem;
text-transform: uppercase;
}
`

@ -59,11 +59,10 @@ export const Content = styled.div`
export const BodyBackdrop = styled.div` export const BodyBackdrop = styled.div`
position: fixed; position: fixed;
// высота хедера top: 8.5rem;
top: ${isMobileDevice ? '124px' : '8.5rem'} ;
left: 0; left: 0;
background-color: rgba(0, 0, 0, 0.7); background-color: rgba(0, 0, 0, 0.7);
width: 100vw; width: 100vw;
height: 100vh; height: calc(100vh - 8.5rem);
z-index: 2; z-index: 2;
` `

@ -22,6 +22,7 @@ export const defaultHeaderStyles = (
ClientNames.Tunisia, ClientNames.Tunisia,
ClientNames.Facr, ClientNames.Facr,
ClientNames.Fqtv, ClientNames.Fqtv,
ClientNames.Rustat,
].includes(client.name) ].includes(client.name)
&& !isMatchPage()) { && !isMatchPage()) {
return client.styles.homePageHeader return client.styles.homePageHeader
@ -76,6 +77,7 @@ export const HeaderStyled = styled.header<HeaderProps>`
client.name === ClientNames.Lff client.name === ClientNames.Lff
|| client.name === ClientNames.Facr || client.name === ClientNames.Facr
|| client.name === ClientNames.Fqtv || client.name === ClientNames.Fqtv
|| client.name === ClientNames.Rustat
|| client.name === ClientNames.Tunisia || client.name === ClientNames.Tunisia
? css` ? css`
background: linear-gradient(187deg, ${color} -4.49%, #000000 68.29%), #000000; background: linear-gradient(187deg, ${color} -4.49%, #000000 68.29%), #000000;

@ -21,7 +21,6 @@ export const ScSportsFilter = styled.div`
justify-content: space-between; justify-content: space-between;
min-width: 15.4%; min-width: 15.4%;
padding-left: 5px; padding-left: 5px;
align-items: flex-start;
${isMobileDevice ${isMobileDevice
? css` ? css`

@ -3,10 +3,8 @@ import { Settings } from 'features/StreamPlayer/components/Settings'
import { AirPlay } from 'features/AirPlay' import { AirPlay } from 'features/AirPlay'
import { ChromeCast } from 'features/ChromeCast' import { ChromeCast } from 'features/ChromeCast'
import { AudioTracks } from 'features/AudioTracks' import { AudioTracks } from 'features/AudioTracks'
import { useLikes } from 'features/MatchPage/store/hooks/useLikes'
import { ControlsPropsExtended } from '../..' import { ControlsPropsExtended } from '../..'
import { LikeButton } from '../../../LikeButton'
import { LiveBtn } from '../../../../styled' import { LiveBtn } from '../../../../styled'
import { import {
Controls, Controls,
@ -24,22 +22,18 @@ export const ControlsMobile = (controlsProps: {props: ControlsPropsExtended}) =>
backToLive, backToLive,
changeAudioTrack, changeAudioTrack,
controlsVisible, controlsVisible,
isFullMatchChapter,
isFullscreen, isFullscreen,
isLive, isLive,
isLiveTime, isLiveTime,
likeButtonRef,
onFullscreenClick, onFullscreenClick,
onQualitySelect, onQualitySelect,
playBackTime, playBackTime,
progressBarElement, progressBarElement,
selectedAudioTrack, selectedAudioTrack,
selectedQuality, selectedQuality,
setGenerateParticles,
videoQualities, videoQualities,
videoRef, videoRef,
} = props } = props
const { canLike, likeClick } = useLikes()
return ( return (
<Controls isFullscreen={isFullscreen}> <Controls isFullscreen={isFullscreen}>
@ -48,14 +42,6 @@ export const ControlsMobile = (controlsProps: {props: ControlsPropsExtended}) =>
{playBackTime} {playBackTime}
</PlaybackTime> </PlaybackTime>
{canLike && isFullMatchChapter && (
<LikeButton
ref={likeButtonRef}
setGenerateParticles={setGenerateParticles}
onClick={likeClick}
/>
)}
<ControlsGroup> <ControlsGroup>
<AudioTracks <AudioTracks
audioTracks={audioTracks!} audioTracks={audioTracks!}

@ -25,7 +25,6 @@ export const Controls = styled.div<FullscreenProps>`
` `
export const ControlsRow = styled.div` export const ControlsRow = styled.div`
position: relative;
width: 100%; width: 100%;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;

@ -10,13 +10,11 @@ import { Settings } from 'features/StreamPlayer/components/Settings'
import { T9n } from 'features/T9n' import { T9n } from 'features/T9n'
import { ChromeCast } from 'features/ChromeCast' import { ChromeCast } from 'features/ChromeCast'
import { AudioTracks } from 'features/AudioTracks' import { AudioTracks } from 'features/AudioTracks'
import { useLikes } from 'features/MatchPage/store/hooks/useLikes'
import { PiP } from 'components/PictureInPicture/PiP' import { PiP } from 'components/PictureInPicture/PiP'
import { ControlsPropsExtended } from '../..' import { ControlsPropsExtended } from '../..'
import { VolumeBar } from '../../../VolumeBar' import { VolumeBar } from '../../../VolumeBar'
import { LikeButton } from '../../../LikeButton'
import { import {
Backward, Backward,
Controls, Controls,
@ -38,13 +36,11 @@ export const ControlsWeb = (controlsProps: { props: ControlsPropsExtended }) =>
changeAudioTrack, changeAudioTrack,
controlsVisible, controlsVisible,
isFirstChapterPlaying, isFirstChapterPlaying,
isFullMatchChapter,
isFullscreen, isFullscreen,
isLastChapterPlaying, isLastChapterPlaying,
isLive, isLive,
isLiveTime, isLiveTime,
isStorage, isStorage,
likeButtonRef,
muted, muted,
numberOfChapters = 0, numberOfChapters = 0,
onFullscreenClick, onFullscreenClick,
@ -60,25 +56,16 @@ export const ControlsWeb = (controlsProps: { props: ControlsPropsExtended }) =>
rewindForward, rewindForward,
selectedAudioTrack, selectedAudioTrack,
selectedQuality, selectedQuality,
setGenerateParticles,
togglePlaying, togglePlaying,
videoQualities, videoQualities,
videoRef, videoRef,
volumeInPercent, volumeInPercent,
} = props } = props
const { canLike, likeClick } = useLikes()
return ( return (
<Controls visible={controlsVisible}> <Controls visible={controlsVisible}>
<ControlsRow> <ControlsRow>
{progressBarElement} {progressBarElement}
{canLike && isFullMatchChapter && (
<LikeButton
ref={likeButtonRef}
setGenerateParticles={setGenerateParticles}
onClick={likeClick}
/>
)}
</ControlsRow> </ControlsRow>
<ControlsRow> <ControlsRow>
<ControlsGroup> <ControlsGroup>

@ -24,13 +24,11 @@ export type ControlsProps = {
controlsVisible: boolean, controlsVisible: boolean,
duration: number, duration: number,
isFirstChapterPlaying?: boolean, isFirstChapterPlaying?: boolean,
isFullMatchChapter?: boolean,
isFullscreen: boolean, isFullscreen: boolean,
isLastChapterPlaying?: boolean, isLastChapterPlaying?: boolean,
isLive?: boolean, isLive?: boolean,
isLiveTime?: boolean, isLiveTime?: boolean,
isStorage?: boolean, isStorage?: boolean,
likeButtonRef: React.RefObject<HTMLButtonElement>,
liveChapters?: LiveChapters, liveChapters?: LiveChapters,
loadedProgress: number, loadedProgress: number,
muted: boolean, muted: boolean,
@ -50,7 +48,6 @@ export type ControlsProps = {
rewindForward: () => void, rewindForward: () => void,
selectedAudioTrack?: MediaPlaylist, selectedAudioTrack?: MediaPlaylist,
selectedQuality: string, selectedQuality: string,
setGenerateParticles: (state: boolean) => void,
src?: string, src?: string,
togglePlaying: () => void, togglePlaying: () => void,
videoQualities: Array<string>, videoQualities: Array<string>,

@ -1,164 +0,0 @@
import {
forwardRef,
memo,
useEffect,
useRef,
useState,
} from 'react'
import { isMobileDevice } from 'config'
import { defaultTheme } from 'features/Theme/config'
import {
Button,
Svg,
Path,
} from './styled'
const COLOR_PAIRS = [
['#FF0000', '#FFDF00'],
['#70FF00', '#0001FF'],
['#CD00FF', defaultTheme.colors.white],
[defaultTheme.colors.white, defaultTheme.colors.white],
[defaultTheme.colors.white, defaultTheme.colors.white],
[defaultTheme.colors.white, defaultTheme.colors.white],
['#FF0000', '#FFDF00'],
['#70FF00', '#0001FF'],
['#CD00FF', defaultTheme.colors.white],
[],
]
const OFFSET_CHANGE_SPEED = isMobileDevice ? 20 : 5
const ANIMATION_INTERVAL = 5000
const FREEZE_TIME = 6000
const PARTICLES_GENERATION_TIME = 1500
type Props = {
onClick: () => void,
setGenerateParticles: (state: boolean) => void,
}
const LikeButtonFC = forwardRef<HTMLButtonElement, Props>((
{
onClick,
setGenerateParticles,
},
ref,
) => {
const stop1Ref = useRef<SVGStopElement>(null)
const stop2Ref = useRef<SVGStopElement>(null)
const [canAnimate, setCanAnimate] = useState(false)
const [isDisabled, setIsDisabled] = useState(false)
const freezeTimeoutIdRef = useRef<NodeJS.Timeout | null>(null)
const generateTimeoutIdRef = useRef<NodeJS.Timeout | null>(null)
const startGenerateParticles = () => setGenerateParticles(true)
const stopGenerateParticles = () => setGenerateParticles(false)
const handleClick = () => {
onClick()
setIsDisabled(true)
startGenerateParticles()
freezeTimeoutIdRef.current = setTimeout(() => setIsDisabled(false), FREEZE_TIME)
generateTimeoutIdRef.current = setTimeout(stopGenerateParticles, PARTICLES_GENERATION_TIME)
}
useEffect(() => {
let requestAnimationId: number
let timeoutId: NodeJS.Timeout
let index = 0
let offset = 100
const animate = () => {
if (!stop1Ref.current || !stop2Ref.current) return
stop1Ref.current.setAttribute('offset', `${offset}%`)
stop2Ref.current.setAttribute('offset', `${offset}%`)
stop1Ref.current.setAttribute('stop-color', `${COLOR_PAIRS[index][0]}`)
stop2Ref.current.setAttribute('stop-color', `${COLOR_PAIRS[index][1]}`)
offset = Math.max(offset - OFFSET_CHANGE_SPEED, 0)
if (offset === 0) {
offset = 100
index = index + 1 <= COLOR_PAIRS.length - 1
? index + 1
: 0
}
if (index === COLOR_PAIRS.length - 1 && offset === 100) {
requestAnimationId && cancelAnimationFrame(requestAnimationId)
setCanAnimate(false)
index = 0
timeoutId = setTimeout(() => {
requestAnimationId = requestAnimationFrame(animate)
}, ANIMATION_INTERVAL)
} else {
requestAnimationId = requestAnimationFrame(animate)
setCanAnimate(true)
}
}
timeoutId = setTimeout(animate, ANIMATION_INTERVAL)
return () => {
timeoutId && clearTimeout(timeoutId)
requestAnimationId && cancelAnimationFrame(requestAnimationId)
}
}, [])
useEffect(() => () => {
freezeTimeoutIdRef.current && clearTimeout(freezeTimeoutIdRef.current)
generateTimeoutIdRef.current && clearTimeout(generateTimeoutIdRef.current)
}, [])
return (
<Button
disabled={isDisabled}
canAnimate={canAnimate}
ref={ref}
aria-label='Like this moment'
onClick={handleClick}
>
<Svg
width='30'
height='30'
viewBox='0 0 30 30'
fill='none'
>
<defs>
<linearGradient id='gradient'>
<stop
ref={stop1Ref}
offset='50%'
stopColor={defaultTheme.colors.white}
/>
<stop
ref={stop2Ref}
offset='50%'
stopColor={defaultTheme.colors.white}
/>
</linearGradient>
</defs>
<g>
<Path
fill={canAnimate ? 'url(#gradient)' : defaultTheme.colors.white}
d='M17.0939 0.686035C15.6852 0.686035 16.1589 3.64594 16.1589 3.64594C16.1589 3.64594 13.1482 11.8087 10.2685 13.9842C9.65136 14.6215 9.26489 15.3262 9.02179 15.9023C8.96569 16.0432 8.91583 16.178 8.87219 16.3006C8.67896 16.6867 8.26757 17.2811 7.38867 17.802L10.2685 30.1135C10.2685 30.1135 14.7252 30.6834 19.1945 30.5915C20.9835 30.7324 22.8784 30.7447 24.3868 30.426C29.5106 29.3536 28.2265 25.8422 28.2265 25.8422C30.9879 23.8015 29.4171 21.2522 29.4171 21.2522C31.873 18.7335 29.4607 16.6193 29.4607 16.6193C29.4607 16.6193 30.7884 14.5847 29.0743 13.0466C26.9363 11.1223 21.1331 12.4031 21.1331 12.4031C20.7279 12.4705 20.2978 12.5563 19.8365 12.6666C19.8365 12.6666 17.8294 13.5858 19.8365 7.59861C21.8499 1.61139 18.5026 0.686035 17.0939 0.686035ZM8.19893 29.2249L6.01728 19.0338C5.89261 18.4516 5.29422 17.9736 4.68959 17.9736H0.525765L0.519531 30.279H7.32003C7.9309 30.2851 8.32359 29.8071 8.19893 29.2249Z'
/>
</g>
</Svg>
</Button>
)
})
export const LikeButton = memo(LikeButtonFC)

@ -1,78 +0,0 @@
import styled, { css, keyframes } from 'styled-components/macro'
import { isMobileDevice } from 'config'
const wiggle = keyframes`
0%, 7% {
transform: rotateZ(0);
}
15% {
transform: rotateZ(-15deg);
}
20% {
transform: rotateZ(10deg);
}
25% {
transform: rotateZ(-10deg);
}
30% {
transform: rotateZ(6deg);
}
35% {
transform: rotateZ(-4deg);
}
40%, 100% {
transform: rotateZ(0);
}
`
export const Svg = styled.svg``
export const Path = styled.path``
export const Button = styled.button<{ canAnimate?: boolean }>`
position: absolute;
bottom: 25px;
right: 22px;
width: 30px;
height: 30px;
padding: 0;
border: none;
background: none;
cursor: pointer;
${({ canAnimate }) => (canAnimate
? css`
animation: ${wiggle} 1s linear infinite;
`
: '')};
${isMobileDevice && css`
width: 25px;
height: 25px;
bottom: 40px;
${Svg} {
width: 25px;
height: 25px;
}
`}
:disabled {
opacity: 0.5;
cursor: initial;
animation: none;
${Path} {
fill: ${({ theme }) => theme.colors.white};
}
}
:hover {
animation: none;
${Path} {
fill: ${({ theme }) => theme.colors.white};
}
}
`

@ -51,7 +51,6 @@ import { useControlsVisibility } from './useControlsVisibility'
import { useProgressChangeHandler } from './useProgressChangeHandler' import { useProgressChangeHandler } from './useProgressChangeHandler'
import { usePlayingHandlers } from './usePlayingHandlers' import { usePlayingHandlers } from './usePlayingHandlers'
import { useAudioTrack } from './useAudioTrack' import { useAudioTrack } from './useAudioTrack'
import { useParticles } from './useParticles'
import { FULL_GAME_KEY } from '../../MatchPage/helpers/buildPlaylists' import { FULL_GAME_KEY } from '../../MatchPage/helpers/buildPlaylists'
export type PlayerState = typeof initialState export type PlayerState = typeof initialState
@ -120,7 +119,6 @@ export const useVideoPlayer = ({
profile, profile,
selectedPlaylist, selectedPlaylist,
setCircleAnimation, setCircleAnimation,
setEpisodeInfo,
setIsFullScreen, setIsFullScreen,
setPlayingProgress, setPlayingProgress,
} = useMatchPageStore() } = useMatchPageStore()
@ -402,7 +400,6 @@ export const useVideoPlayer = ({
seek: pausedProgress.current / 1000, seek: pausedProgress.current / 1000,
}) })
}, 100) }, 100)
setEpisodeInfo({})
} }
useEffect(() => { useEffect(() => {
@ -747,6 +744,5 @@ export const useVideoPlayer = ({
...useVolume(), ...useVolume(),
...useVideoQuality(hls), ...useVideoQuality(hls),
...useAudioTrack(hls), ...useAudioTrack(hls),
...useParticles(),
} }
} }

@ -1,138 +0,0 @@
import {
useEffect,
useRef,
useState,
} from 'react'
import { isMobileDevice } from 'config'
type Particle = {
height: number, // Высота частицы
image: HTMLImageElement, // Изображение частицы
life: number, // Время жизни частицы
opacity: number, // Непрозрачность частицы,
vx: number, // Горизонтальная скорость частицы
vy: number, // Вертикальная скорость частицы
width: number, // Ширина частицы
x: number, // Cмещение по горизонтали
y: number, // Смещение по вертикали
}
const BIRTH_RATE = 100
const PARTICLES_COUNT = isMobileDevice ? 1 : 2
export const useParticles = () => {
const canvasRef = useRef<HTMLCanvasElement>(null)
const likeButtonRef = useRef<HTMLButtonElement>(null)
const requestAnimationIdRef = useRef<number | null>(null)
const [generateParticles, setGenerateParticles] = useState(false)
useEffect(() => {
const canvas = canvasRef.current
const likeButton = likeButtonRef.current
if (!canvas || !likeButton) return () => {}
let intervalId: NodeJS.Timeout
const particleImage = new Image()
particleImage.src = '/images/like-active-icon.svg'
const ctx = canvas.getContext('2d')
if (!ctx) return () => {}
const canvasRect = canvas.getBoundingClientRect()
const likeButtonRect = likeButton.getBoundingClientRect()
// Массив для хранения частиц
const particles: Array<Particle> = []
// Начальные координаты вылета частиц
const startX = (likeButtonRect.left - canvasRect.left) + likeButtonRect.width / 2
const startY = likeButtonRect.top - canvasRect.top
// Функция для создания частиц
const createParticles = (count: number) => {
for (let i = 0; i < count; i++) {
const particleSize = Math.random() * (isMobileDevice ? 30 : 40)
const particle: Particle = {
height: particleSize,
image: particleImage,
life: 500,
opacity: 0.5,
vx: 0,
vy: -(Math.random() * 2 + 2),
width: particleSize,
x: startX - particleSize / 2 + Math.random() * 40 - 20,
y: startY,
}
particles.push(particle)
}
}
// Функция для обновления частиц
const updateParticles = () => {
ctx.clearRect(
0,
0,
canvas.width,
canvas.height,
)
for (let i = 0; i < particles.length; i++) {
const particle = particles[i]
particle.life-- // уменьшаем время жизни частицы
// Удаляем частицу из массива, если её время жизни истекло
if (particle.life <= 0) {
particles.splice(i, 1)
i--
} else {
particle.x += particle.vx // Обновляем положение частицы по горизонтали
particle.y += particle.vy // Обновляем положение частицы по вертикали
ctx.globalAlpha = particle.opacity
ctx.drawImage(
particle.image,
particle.x,
particle.y,
particle.width,
particle.height,
)
}
}
if (particles.length === 0 && requestAnimationIdRef.current) {
cancelAnimationFrame(requestAnimationIdRef.current)
return
}
requestAnimationIdRef.current = requestAnimationFrame(updateParticles)
}
if (generateParticles) {
createParticles(PARTICLES_COUNT)
intervalId = setInterval(() => createParticles(PARTICLES_COUNT), BIRTH_RATE)
updateParticles()
}
return () => {
intervalId && clearInterval(intervalId)
}
}, [generateParticles])
useEffect(() => () => {
requestAnimationIdRef.current && cancelAnimationFrame(requestAnimationIdRef.current)
}, [])
return {
canvasRef,
likeButtonRef,
setGenerateParticles,
}
}

@ -1,7 +1,6 @@
import { Fragment, memo } from 'react' import { Fragment } from 'react'
import includes from 'lodash/includes' import includes from 'lodash/includes'
import isEmpty from 'lodash/isEmpty'
import { isMobileDevice } from 'config' import { isMobileDevice } from 'config'
@ -38,11 +37,8 @@ import {
EpisodeInfoDivider, EpisodeInfoDivider,
CloseButton, CloseButton,
PlayerAdsWrapper, PlayerAdsWrapper,
Canvas,
} from './styled' } from './styled'
const CanvasComponent = memo(Canvas)
const tournamentsWithWatermark = { const tournamentsWithWatermark = {
316: 'Tunisia', 316: 'Tunisia',
1136: 'Brasil', 1136: 'Brasil',
@ -69,7 +65,6 @@ export const StreamPlayer = (props: Props) => {
audioTracks, audioTracks,
backToLive, backToLive,
buffering, buffering,
canvasRef,
centerControlsVisible, centerControlsVisible,
changeAudioTrack, changeAudioTrack,
chapters, chapters,
@ -79,7 +74,6 @@ export const StreamPlayer = (props: Props) => {
isFullscreen, isFullscreen,
isLive, isLive,
isLiveTime, isLiveTime,
likeButtonRef,
loadedProgress, loadedProgress,
mainControlsVisible, mainControlsVisible,
muted, muted,
@ -111,7 +105,6 @@ export const StreamPlayer = (props: Props) => {
seek, seek,
selectedAudioTrack, selectedAudioTrack,
selectedQuality, selectedQuality,
setGenerateParticles,
showCenterControls, showCenterControls,
stopPlayingEpisodes, stopPlayingEpisodes,
togglePlaying, togglePlaying,
@ -146,8 +139,6 @@ export const StreamPlayer = (props: Props) => {
)} )}
{isPlayingEpisode && ( {isPlayingEpisode && (
<EpisodeInfo> <EpisodeInfo>
{!isEmpty(episodeInfo) && (
<Fragment>
<EpisodeInfoName> <EpisodeInfoName>
{isMobileDevice {isMobileDevice
? ( ? (
@ -166,8 +157,6 @@ export const StreamPlayer = (props: Props) => {
{episodeInfo.episodesCount} {episodeInfo.episodesCount}
</EpisodeInfoOrder> </EpisodeInfoOrder>
)} )}
</Fragment>
)}
<CloseButton onClick={stopPlayingEpisodes} /> <CloseButton onClick={stopPlayingEpisodes} />
</EpisodeInfo> </EpisodeInfo>
)} )}
@ -269,9 +258,6 @@ export const StreamPlayer = (props: Props) => {
activeChapterIndex={activeChapterIndex} activeChapterIndex={activeChapterIndex}
liveChapters={chapters} liveChapters={chapters}
selectedAudioTrack={selectedAudioTrack} selectedAudioTrack={selectedAudioTrack}
setGenerateParticles={setGenerateParticles}
likeButtonRef={likeButtonRef}
isFullMatchChapter={chapters[0].isFullMatchChapter}
/> />
<ControlsGradient isVisible={mainControlsVisible} /> <ControlsGradient isVisible={mainControlsVisible} />
{!user && ( {!user && (
@ -283,13 +269,6 @@ export const StreamPlayer = (props: Props) => {
videoRef={videoRef} videoRef={videoRef}
/> />
)} )}
{user && chapters[0].isFullMatchChapter && (
<CanvasComponent
ref={canvasRef}
width={wrapperRef.current?.clientWidth}
height={wrapperRef.current?.clientHeight}
/>
)}
</PlayerWrapper> </PlayerWrapper>
) )
} }

@ -55,7 +55,6 @@ export const Controls = styled.div`
` `
export const ControlsRow = styled.div` export const ControlsRow = styled.div`
position: relative;
width: 100%; width: 100%;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
@ -495,10 +494,3 @@ export const PlayerAdsWrapper = styled.div<PlayerAdsProps>`
opacity: ${({ isFullscreen }) => (isFullscreen ? 0 : 1)}; opacity: ${({ isFullscreen }) => (isFullscreen ? 0 : 1)};
` `
export const Canvas = styled.canvas`
position: absolute;
top: 0;
bottom: 0;
right: 0;
pointer-events: none;
`

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save