in-142-fixes #11

Closed
ruslan.khairullin wants to merge 31 commits from in-142-fixes into in-142
  1. 190
      .drone.yml
  2. 3
      .gitignore
  3. 79
      Makefile
  4. 2
      package.json
  5. 3
      public/images/matchTabs/bets.svg
  6. 3
      public/images/matchTabs/chat.svg
  7. 6
      public/images/matchTabs/players.svg
  8. 4
      public/images/matchTabs/plays.svg
  9. 12
      public/images/matchTabs/stats.svg
  10. 3
      public/images/matchTabs/watch.svg
  11. 29
      public/images/tunis-logo.svg
  12. 36
      public/images/tunis_auth_logo.svg
  13. 17
      public/index.html
  14. 9
      run.sh
  15. 18
      src/components/PictureInPicture/PiP.tsx
  16. 5
      src/components/SmartBanner/index.tsx
  17. 6
      src/components/SportIcon/SportIcon.tsx
  18. 6
      src/config/clients/index.tsx
  19. 16
      src/config/clients/india.tsx
  20. 1
      src/config/clients/insports.tsx
  21. 57
      src/config/clients/tunis.tsx
  22. 6
      src/config/clients/types.tsx
  23. 2
      src/config/env.tsx
  24. 7
      src/config/lexics/indexLexics.tsx
  25. 13
      src/config/routes.tsx
  26. 2
      src/config/userAgent.tsx
  27. 3
      src/features/AuthServiceApp/components/ConfirmPopup/styled.tsx
  28. 8
      src/features/AuthServiceApp/components/RegisterPopup/index.tsx
  29. 23
      src/features/AuthServiceApp/components/RegisterPopup/styled.tsx
  30. 4
      src/features/AuthServiceApp/config/clients/index.tsx
  31. 9
      src/features/AuthServiceApp/config/clients/india.tsx
  32. 67
      src/features/AuthServiceApp/config/clients/tunis.tsx
  33. 1
      src/features/AuthServiceApp/config/clients/types.tsx
  34. 12
      src/features/AuthStore/helpers.tsx
  35. 2
      src/features/CompanyInfo/index.tsx
  36. 16
      src/features/HeaderMobile/index.tsx
  37. 4
      src/features/HomePage/hooks.tsx
  38. 2
      src/features/HomePage/index.tsx
  39. 9
      src/features/MatchPage/components/FinishedMatch/index.tsx
  40. 8
      src/features/MatchPage/components/LiveMatch/hooks/index.tsx
  41. 1
      src/features/MatchPage/index.tsx
  42. 18
      src/features/MatchPage/store/hooks/index.tsx
  43. 28
      src/features/MatchPage/store/hooks/useMatchData.tsx
  44. 159
      src/features/MatchPage/store/hooks/usePlayersStats.tsx
  45. 12
      src/features/MatchPage/store/hooks/useStatsTab.tsx
  46. 78
      src/features/MatchPage/store/hooks/useTeamsStats.tsx
  47. 31
      src/features/MatchSidePlaylists/components/MatchPlaylists/index.tsx
  48. 0
      src/features/MatchSidePlaylists/components/Matches/components/VideoDate/index.tsx
  49. 0
      src/features/MatchSidePlaylists/components/Matches/components/VideoDate/styled.tsx
  50. 14
      src/features/MatchSidePlaylists/components/Matches/index.tsx
  51. 9
      src/features/MatchSidePlaylists/components/Matches/styled.tsx
  52. 5
      src/features/MatchSidePlaylists/components/PlayersPlaylists/index.tsx
  53. 6
      src/features/MatchSidePlaylists/components/PlayersPlaylists/styled.tsx
  54. 7
      src/features/MatchSidePlaylists/components/PlayersTable/config.tsx
  55. 58
      src/features/MatchSidePlaylists/components/PlayersTable/hooks/index.tsx
  56. 83
      src/features/MatchSidePlaylists/components/PlayersTable/hooks/usePlayers.tsx
  57. 171
      src/features/MatchSidePlaylists/components/PlayersTable/hooks/useTable.tsx
  58. 166
      src/features/MatchSidePlaylists/components/PlayersTable/index.tsx
  59. 241
      src/features/MatchSidePlaylists/components/PlayersTable/styled.tsx
  60. 8
      src/features/MatchSidePlaylists/components/PlayersTable/types.tsx
  61. 31
      src/features/MatchSidePlaylists/components/TabPlayers/index.tsx
  62. 10
      src/features/MatchSidePlaylists/components/TabStats/config.tsx
  63. 68
      src/features/MatchSidePlaylists/components/TabStats/hooks.tsx
  64. 103
      src/features/MatchSidePlaylists/components/TabStats/index.tsx
  65. 110
      src/features/MatchSidePlaylists/components/TabStats/styled.tsx
  66. 54
      src/features/MatchSidePlaylists/components/TabWatch/index.tsx
  67. 31
      src/features/MatchSidePlaylists/components/TeamsStatsTable/hooks.tsx
  68. 93
      src/features/MatchSidePlaylists/components/TeamsStatsTable/index.tsx
  69. 65
      src/features/MatchSidePlaylists/components/TeamsStatsTable/styled.tsx
  70. 3
      src/features/MatchSidePlaylists/config.tsx
  71. 60
      src/features/MatchSidePlaylists/hooks.tsx
  72. 50
      src/features/MatchSidePlaylists/index.tsx
  73. 83
      src/features/MatchSidePlaylists/styled.tsx
  74. 44
      src/features/MultiSourcePlayer/hooks/index.tsx
  75. 2
      src/features/StreamPlayer/components/Controls/Components/ControlsWeb/index.tsx
  76. 4
      src/features/StreamPlayer/components/YoutubePlayer/index.tsx
  77. 37
      src/features/StreamPlayer/hooks/index.tsx
  78. 4
      src/features/StreamPlayer/index.tsx
  79. 2
      src/features/Tooltip/index.tsx
  80. 2
      src/features/UserAccount/index.tsx
  81. 25
      src/helpers/getTeamAbbr/index.tsx
  82. 1
      src/helpers/index.tsx
  83. 1
      src/hooks/index.tsx
  84. 1
      src/hooks/usePageParams.tsx
  85. 2
      src/react-app-env.d.ts
  86. 65
      src/requests/getMatchParticipants.tsx
  87. 54
      src/requests/getPlayersStats.tsx
  88. 56
      src/requests/getTeamsStats.tsx
  89. 1
      src/requests/getUserInfo.tsx
  90. 4
      src/requests/index.tsx
  91. 30
      src/requests/saveMatchStats.tsx

@ -15,9 +15,28 @@ trigger:
- refs/heads/master
steps:
- name: deploy script
- 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-prod
image: node:16-alpine
environment:
REACT_APP_STRIPE_PK:
from_secret: REACT_APP_STRIPE_PK
commands:
- apk add --no-cache make
- make prod
depends_on:
- npm-install
- name: deploy-prod
image: amazon/aws-cli:latest
environment:
AWS_ACCESS_KEY_ID:
from_secret: AWS_ACCESS_KEY_ID
@ -25,27 +44,162 @@ steps:
from_secret: AWS_SECRET_ACCESS_KEY
AWS_DEFAULT_REGION:
from_secret: AWS_DEFAULT_REGION
REACT_APP_STRIPE_PK:
from_secret: REACT_APP_STRIPE_PK
SSH_KEY_AUTH:
from_secret: SSH_KEY_AUTH
AWS_MAX_ATTEMPTS: 10
commands:
- apk add --no-cache aws-cli bash git openssh-client make rsync
- npm install --legacy-peer-deps
- make prod
# - aws s3 sync build s3://insports-prod --delete
# - aws cloudfront create-invalidation --distribution-id E3KY6BCU3AYHEW --paths "/*"
- aws s3 sync build s3://instat-frontend-test-a --delete
- aws cloudfront create-invalidation --distribution-id E1WZHVCHZ48SG6 --paths "/*"
depends_on:
- make-prod
- name: make-auth
image: node:16-alpine
environment:
REACT_APP_STRIPE_PK:
from_secret: REACT_APP_STRIPE_PK
commands:
- apk add --no-cache make
- make auth-production-build
depends_on:
- npm-install
- name: deploy-S3-auth
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 s3://insports-auth --delete
- aws cloudfront create-invalidation --distribution-id EERIKX9X2SRPJ --paths "/*"
depends_on:
- make-auth
- name: deploy-old-auth-server
image: node:16-alpine
environment:
SSH_KEY_AUTH:
from_secret: SSH_KEY_AUTH
commands:
- apk add --no-cache openssh-client rsync
- eval $(ssh-agent -s)
- echo -n "$SSH_KEY_AUTH" | tr -d '\r' | ssh-add -
- mkdir -p ~/.ssh && chmod 700 ~/.ssh
- ssh-keyscan auth.insports.tv >> ~/.ssh/known_hosts
- rsync -v -r -C build_auth/ ubuntu@auth.insports.tv:/home/ubuntu/ott-auth/src/frontend/
- rsync -v -r -C build_auth/clients/* ubuntu@auth.insports.tv:/home/ubuntu/ott-auth/src/frontend/templates
- ssh ubuntu@auth.insports.tv 'bash -s' < ./run.sh OTT-2535 docker-compose-stage.yaml
depends_on:
- make-auth
- name: make-india
image: node:16-alpine
environment:
REACT_APP_STRIPE_PK:
from_secret: REACT_APP_STRIPE_PK
commands:
- apk add --no-cache make
- make india-prod
depends_on:
- npm-install
- name: deploy-india
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_india s3://insports-india --delete
- aws cloudfront create-invalidation --distribution-id E5DKN8IPOMASO --paths "/*"
depends_on:
- make-india
- name: make-facr
image: node:16-alpine
environment:
REACT_APP_STRIPE_PK:
from_secret: REACT_APP_STRIPE_PK
commands:
- apk add --no-cache make
- make facr-prod
depends_on:
- npm-install
- name: deploy-facr
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_facr s3://insports-facr-tv --delete
- aws cloudfront create-invalidation --distribution-id E1ZYJS9RAJO89D --paths "/*"
depends_on:
- make-facr
- name: make-lff
image: node:16-alpine
environment:
REACT_APP_STRIPE_PK:
from_secret: REACT_APP_STRIPE_PK
commands:
- apk add --no-cache make
- make lff-prod
depends_on:
- npm-install
- name: deploy-lff
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_lff s3://insports-tv-lff-lv --delete
- aws cloudfront create-invalidation --distribution-id E2127IDW4TEH4S --paths "/*"
depends_on:
- make-lff
# - name: make-diwansport
# image: node:16-alpine
# environment:
# REACT_APP_STRIPE_PK:
# from_secret: REACT_APP_STRIPE_PK
# commands:
# - apk add --no-cache make
# - make diwansport-prod
# depends_on:
# - npm-install
#
# - name: deploy-diwansport
# 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_insports-diwansport s3://insports-diwansport --delete
# - aws cloudfront create-invalidation --distribution-id E3LKAH6TR4O2JL --paths "/*"
# depends_on:
# - make-diwansport
---
kind: pipeline
@ -131,7 +285,6 @@ steps:
## - ssh-keyscan auth.insports.tv >> ~/.ssh/known_hosts
## - rsync -v -r -C build_auth/ ubuntu@auth.insports.tv:/home/ubuntu/ott-auth/src/frontend/
## - rsync -v -r -C build_auth/clients/* ubuntu@auth.insports.tv:/home/ubuntu/ott-auth/src/frontend/templates
## - ssh ubuntu@auth.insports.tv 'bash -s' < ./run.sh OTT-2535 docker-compose-stage.yaml
---
@ -523,17 +676,18 @@ steps:
from_secret: AWS_DEFAULT_REGION
REACT_APP_STRIPE_PK:
from_secret: REACT_APP_STRIPE_PK_TEST
SSH_KEY_AUTH:
from_secret: SSH_KEY_AUTH
SSH_KEY_AUTH_TEST:
from_secret: SSH_KEY_AUTH_TEST
commands:
- apk add --no-cache aws-cli bash git openssh-client make rsync
- npm install --legacy-peer-deps
- make auth-build
- eval $(ssh-agent -s)
- echo -n "$SSH_KEY_AUTH" | tr -d '\r' | ssh-add -
- echo -n "$SSH_KEY_AUTH_TEST" | tr -d '\r' | ssh-add -
- mkdir -p ~/.ssh && chmod 700 ~/.ssh
- ssh-keyscan auth.test.insports.tv >> ~/.ssh/known_hosts
- rsync -v -r -C build_auth/ ubuntu@auth.test.insports.tv:/home/ubuntu/ott-auth/src/frontend/
- rsync -v -r -C build_auth/clients/* ubuntu@auth.test.insports.tv:/home/ubuntu/ott-auth/src/frontend/templates
- ssh ubuntu@auth.test.insports.tv 'bash -s' < ./run.sh OTT-2535-test docker-compose-test.yaml
- aws s3 sync build_auth s3://auth-insports-test --delete
- aws cloudfront create-invalidation --distribution-id E10YI3RFOZZDLZ --paths "/*"

3
.gitignore vendored

@ -11,6 +11,9 @@
# production
/build
/build_auth
/build_india
/build_facr
/build_lff
# misc
.DS_Store

@ -19,7 +19,7 @@ build-stage: clean
REACT_APP_TYPE=ott \
REACT_APP_ENV=preproduction \
REACT_APP_CLIENT=insports \
REACT_APP_STRIPE_PK=pk_live_51J5TEYEDSxVnTgDW7v2lF8GGogrm7XaaICZ9CN876sITIBBauZgB2ommUTUOiY868jzpbhQjZcoBOjIRX5Vs54Aq00y4C3USyB \
REACT_APP_STRIPE_PK=pk_live_51J5TEYEDSxVnTgDW5XxhC6ntKZKddXgKHq5HOCDmJTdfSKluMYCdLHOcUA3Miuy8HesxG1eS4c0dQRQpMsEHRrQL00USpu5xIq \
npm run build
build-a: clean
@ -124,6 +124,12 @@ facr-build: clean
REACT_APP_CLIENT=facr \
npm run build
india-build: clean
REACT_APP_TYPE=ott \
REACT_APP_ENV=staging \
REACT_APP_CLIENT=india \
npm run build
lff-build: clean
REACT_APP_TYPE=ott \
REACT_APP_ENV=staging \
@ -136,75 +142,46 @@ prod: clean
REACT_APP_TYPE=ott \
REACT_APP_ENV=production \
REACT_APP_CLIENT=insports \
REACT_APP_STRIPE_PK=pk_live_51J5TEYEDSxVnTgDW7v2lF8GGogrm7XaaICZ9CN876sITIBBauZgB2ommUTUOiY868jzpbhQjZcoBOjIRX5Vs54Aq00y4C3USyB \
REACT_APP_STRIPE_PK=pk_live_51J5TEYEDSxVnTgDW5XxhC6ntKZKddXgKHq5HOCDmJTdfSKluMYCdLHOcUA3Miuy8HesxG1eS4c0dQRQpMsEHRrQL00USpu5xIq \
npm run build && cp -r .well-known build
# rsync -zavP --delete-before build/ -e 'ssh -p 666' ott@de.instat.tv:/usr/local/www/ott/wwwroot/
# rsync -zavP --delete-before build/ -e 'ssh -p 666' ott@fr.instat.tv:/usr/local/www/ott/wwwroot/
# rsync -zavP --delete-before build/ -e 'ssh -p 666' ott@137.74.33.74:/usr/local/www/ott/wwwroot/
preprod: clean
REACT_APP_TYPE=ott \
REACT_APP_ENV=preproduction \
REACT_APP_STAGE=test \
REACT_APP_CLIENT=insports \
REACT_APP_STRIPE_PK=pk_live_ANI76cBhSo69DZUxPmyRVIZW \
REACT_APP_STRIPE_PK=pk_live_51J5TEYEDSxVnTgDW5XxhC6ntKZKddXgKHq5HOCDmJTdfSKluMYCdLHOcUA3Miuy8HesxG1eS4c0dQRQpMsEHRrQL00USpu5xIq \
npm run build
facr-prod: clean
india-prod:
rm -rf build_india && \
REACT_APP_TYPE=ott \
REACT_APP_ENV=production \
REACT_APP_STRIPE_PK=pk_live_51J5TEYEDSxVnTgDW7v2lF8GGogrm7XaaICZ9CN876sITIBBauZgB2ommUTUOiY868jzpbhQjZcoBOjIRX5Vs54Aq00y4C3USyB \
REACT_APP_CLIENT=facr \
npm run build
REACT_APP_CLIENT=india \
REACT_APP_STRIPE_PK=pk_live_51J5TEYEDSxVnTgDW5XxhC6ntKZKddXgKHq5HOCDmJTdfSKluMYCdLHOcUA3Miuy8HesxG1eS4c0dQRQpMsEHRrQL00USpu5xIq \
BUILD_PATH=build_india \
npm run build && cp -r .well-known build_india
rsync -zavP --delete-before build/ -e 'ssh -p 666' ott@de.instat.tv:/usr/local/www/ott/facr-wwwroot/
rsync -zavP --delete-before build/ -e 'ssh -p 666' ott@fr.instat.tv:/usr/local/www/ott/facr-wwwroot/
rsync -zavP --delete-before build/ -e 'ssh -p 666' ott@137.74.33.74:/usr/local/www/ott/facr-wwwroot/
facr-prod:
rm -rf build_facr && \
REACT_APP_TYPE=ott \
REACT_APP_ENV=production \
REACT_APP_STRIPE_PK=pk_live_51J5TEYEDSxVnTgDW5XxhC6ntKZKddXgKHq5HOCDmJTdfSKluMYCdLHOcUA3Miuy8HesxG1eS4c0dQRQpMsEHRrQL00USpu5xIq \
REACT_APP_CLIENT=facr \
BUILD_PATH=build_facr \
npm run build && cp -r .well-known build_facr
lff-prod: clean
lff-prod:
rm -rf build_lff && \
REACT_APP_TYPE=ott \
REACT_APP_ENV=production \
REACT_APP_STRIPE_PK=pk_live_51J5TEYEDSxVnTgDW7v2lF8GGogrm7XaaICZ9CN876sITIBBauZgB2ommUTUOiY868jzpbhQjZcoBOjIRX5Vs54Aq00y4C3USyB \
REACT_APP_STRIPE_PK=pk_live_51J5TEYEDSxVnTgDW5XxhC6ntKZKddXgKHq5HOCDmJTdfSKluMYCdLHOcUA3Miuy8HesxG1eS4c0dQRQpMsEHRrQL00USpu5xIq \
REACT_APP_CLIENT=lff \
npm run build
rsync -zavP --delete-before build/ -e 'ssh -p 666' ott@de.instat.tv:/usr/local/www/ott/lff-wwwroot/
rsync -zavP --delete-before build/ -e 'ssh -p 666' ott@fr.instat.tv:/usr/local/www/ott/lff-wwwroot/
rsync -zavP --delete-before build/ -e 'ssh -p 666' ott@137.74.33.74:/usr/local/www/ott/lff-wwwroot/
BUILD_PATH=build_lff \
npm run build && cp -r .well-known build_lff
deploy-all: prod preprod facr-prod lff-prod
stage: build-stage
rsync -zavP --delete-before build/ -e 'ssh -p 666' ott-staging@137.74.33.74:/usr/local/www/ott-staging/wwwroot/
a-stage: build-a
rsync -zavP --delete-before build/ -e 'ssh -p 666' ott-staging@137.74.33.74:/usr/local/www/ott-staging/a-wwwroot/
b-stage: build-b
rsync -zavP --delete-before build/ -e 'ssh -p 666' ott-staging@137.74.33.74:/usr/local/www/ott-staging/b-wwwroot/
c-stage: build-c
rsync -zavP --delete-before build/ -e 'ssh -p 666' ott-staging@137.74.33.74:/usr/local/www/ott-staging/c-wwwroot/
d-stage: build-d
rsync -zavP --delete-before build/ -e 'ssh -p 666' ott-staging@137.74.33.74:/usr/local/www/ott-staging/d-wwwroot/
e-stage: build-e
rsync -zavP --delete-before build/ -e 'ssh -p 666' ott-staging@137.74.33.74:/usr/local/www/ott-staging/e-wwwroot/
f-stage: build-f
rsync -zavP --delete-before build/ -e 'ssh -p 666' ott-staging@137.74.33.74:/usr/local/www/ott-staging/f-wwwroot/
g-stage: build-g
rsync -zavP --delete-before build/ -e 'ssh -p 666' ott-staging@137.74.33.74:/usr/local/www/ott-staging/g-wwwroot/
h-stage: build-h
rsync -zavP --delete-before build/ -e 'ssh -p 666' ott-staging@137.74.33.74:/usr/local/www/ott-staging/h-wwwroot/
i-stage: build-i
rsync -zavP --delete-before build/ -e 'ssh -p 666' ott-staging@137.74.33.74:/usr/local/www/ott-staging/i-wwwroot/
test:
npm test

@ -16,6 +16,8 @@
"test:auth": "REACT_APP_TYPE=auth-service react-scripts test",
"facr": "REACT_APP_CLIENT=facr react-scripts start",
"lff": "REACT_APP_CLIENT=lff react-scripts start",
"india": "REACT_APP_CLIENT=india react-scripts start",
"tunis": "REACT_APP_CLIENT=tunis react-scripts start",
"insports": "REACT_APP_CLIENT=insports react-scripts start"
},
"dependencies": {

@ -0,0 +1,3 @@
<svg width="22" height="16" viewBox="0 0 22 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M20.75 6.5C20.9489 6.5 21.1397 6.42098 21.2803 6.28033C21.421 6.13968 21.5 5.94891 21.5 5.75V2C21.5 1.60218 21.342 1.22064 21.0607 0.93934C20.7794 0.658035 20.3978 0.5 20 0.5H2C1.60218 0.5 1.22064 0.658035 0.93934 0.93934C0.658035 1.22064 0.5 1.60218 0.5 2V5.75C0.5 5.94891 0.579018 6.13968 0.71967 6.28033C0.860322 6.42098 1.05109 6.5 1.25 6.5C1.64782 6.5 2.02936 6.65804 2.31066 6.93934C2.59196 7.22064 2.75 7.60218 2.75 8C2.75 8.39782 2.59196 8.77936 2.31066 9.06066C2.02936 9.34196 1.64782 9.5 1.25 9.5C1.05109 9.5 0.860322 9.57902 0.71967 9.71967C0.579018 9.86032 0.5 10.0511 0.5 10.25V14C0.5 14.3978 0.658035 14.7794 0.93934 15.0607C1.22064 15.342 1.60218 15.5 2 15.5H20C20.3978 15.5 20.7794 15.342 21.0607 15.0607C21.342 14.7794 21.5 14.3978 21.5 14V10.25C21.5 10.0511 21.421 9.86032 21.2803 9.71967C21.1397 9.57902 20.9489 9.5 20.75 9.5C20.3522 9.5 19.9706 9.34196 19.6893 9.06066C19.408 8.77936 19.25 8.39782 19.25 8C19.25 7.60218 19.408 7.22064 19.6893 6.93934C19.9706 6.65804 20.3522 6.5 20.75 6.5ZM20 10.9025V14H14.75V11.75H13.25V14H2V10.9025C2.642 10.7347 3.21025 10.3588 3.61582 9.8336C4.02139 9.3084 4.24139 8.66356 4.24139 8C4.24139 7.33644 4.02139 6.6916 3.61582 6.1664C3.21025 5.64121 2.642 5.2653 2 5.0975V2H13.25V4.25H14.75V2H20V5.0975C19.358 5.2653 18.7898 5.64121 18.3842 6.1664C17.9786 6.6916 17.7586 7.33644 17.7586 8C17.7586 8.66356 17.9786 9.3084 18.3842 9.8336C18.7898 10.3588 19.358 10.7347 20 10.9025Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18 0H2C0.9 0 0 0.9 0 2V20L4 16H18C19.1 16 20 15.1 20 14V2C20 0.9 19.1 0 18 0ZM18 14H4L2 16V2H18V14Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 229 B

@ -0,0 +1,6 @@
<svg width="21" height="21" viewBox="0 0 21 21" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M15.1862 14.29C15.1862 14.29 14.4125 13.347 13.7596 13.1778C11.7528 12.67 10.8824 12.0414 10.5681 11.6787C10.689 11.582 10.8099 11.4611 10.9308 11.3402C12.2606 9.84117 12.0672 7.32661 12.043 7.0123C11.8979 3.99001 9.6735 3.26466 8.36787 3.31301C7.06224 3.26466 4.83783 4.01418 4.69276 7.0123C4.66859 7.30244 4.47516 9.81698 5.80497 11.3402C5.92586 11.4853 6.07093 11.6304 6.216 11.7271C6.16764 11.7512 6.14346 11.7754 6.14346 11.7996C5.78079 12.1865 4.93455 12.7184 3.07282 13.2019C2.37164 13.3712 1.59794 14.5076 1.59794 14.5076C1.59794 14.5076 0.928283 15.9908 1.10911 16.1968C2.86924 18.199 5.44868 19.4641 8.31951 19.4641C11.3689 19.4641 14.0895 18.0368 15.8487 15.8148C15.6145 14.8567 15.1862 14.29 15.1862 14.29ZM3.41131 14.5801C2.90357 14.701 2.7585 15.3296 2.7585 15.8374C4.2092 17.1914 6.16764 18.0376 8.31951 18.0376C10.5681 18.0376 12.5991 17.1188 14.0981 15.6681C14.0503 14.7595 13.6004 14.6312 13.4268 14.5817L13.4211 14.5801C12.3331 14.3142 11.5111 13.9998 10.9066 13.6855C10.3021 14.4834 9.35918 14.967 8.34369 14.967C7.35238 14.967 6.4336 14.5076 5.82914 13.7339C5.22469 14.024 4.4268 14.3142 3.41131 14.5801ZM8.36787 10.9775C9.02068 10.9775 9.50425 10.7841 9.86692 10.3731C10.6648 9.47849 10.6648 7.73765 10.5923 7.13319V7.08483C10.5439 6.06934 10.1812 5.36817 9.52843 5.0055C9.02505 4.73093 8.50002 4.73784 8.38365 4.73938C8.37705 4.73946 8.37176 4.73953 8.36787 4.73953H8.27115C8.10191 4.73953 7.61834 4.73953 7.15895 5.0055C6.50614 5.36817 6.14346 6.06934 6.09511 7.08483V7.13319C6.02257 7.73765 6.04675 9.47849 6.84463 10.3731C7.20731 10.7599 7.69087 10.9775 8.34369 10.9775H8.36787ZM8.29533 12.4282C8.02937 12.4282 7.76341 12.4041 7.5458 12.3557C7.42491 12.525 7.25567 12.7426 7.03806 12.936C7.35238 13.3228 7.83594 13.5404 8.34369 13.5404C8.89979 13.5404 9.38336 13.2745 9.72185 12.8635C9.52843 12.67 9.38336 12.5008 9.28664 12.3315C9.02068 12.3799 8.75472 12.4282 8.4404 12.4282H8.29533Z" fill="white"/>
<rect x="13.3672" y="4.10608" width="6.14323" height="1.53581" rx="0.767904" fill="white"/>
<rect x="13.3672" y="6.40979" width="6.14323" height="1.53581" rx="0.767904" fill="white"/>
<rect x="13.3672" y="8.7135" width="6.14323" height="1.53581" rx="0.767904" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

@ -0,0 +1,4 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.9915 0.666626C4.3915 0.666626 0.666504 4.39996 0.666504 8.99996C0.666504 13.6 4.3915 17.3333 8.9915 17.3333C13.5998 17.3333 17.3332 13.6 17.3332 8.99996C17.3332 4.39996 13.5998 0.666626 8.9915 0.666626ZM8.99984 15.6666C5.3165 15.6666 2.33317 12.6833 2.33317 8.99996C2.33317 5.31663 5.3165 2.33329 8.99984 2.33329C12.6832 2.33329 15.6665 5.31663 15.6665 8.99996C15.6665 12.6833 12.6832 15.6666 8.99984 15.6666Z" fill="white"/>
<path d="M9.4165 5.45829C9.4165 5.11311 9.13668 4.83329 8.7915 4.83329C8.44633 4.83329 8.1665 5.11311 8.1665 5.45829V9.2671C8.1665 9.61837 8.3508 9.94387 8.65201 10.1246L12.0317 12.1524C12.3139 12.3217 12.6797 12.2316 12.851 11.9507C13.0246 11.666 12.9321 11.2942 12.6453 11.1241L9.90623 9.49886C9.60263 9.31873 9.4165 8.99187 9.4165 8.63885V5.45829Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 908 B

@ -0,0 +1,12 @@
<svg width="22" height="18" viewBox="0 0 22 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M21.1 0H0.9C0.4 0 0 0.4 0 0.9V17.1C0 17.5 0.4 18 0.9 18H21.1C21.5 18 22 17.5 22 17.1V0.9C22 0.4 21.5 0 21.1 0ZM20 16H2V2H20V16Z" fill="white"/>
<path d="M13.5 4H9C8.44772 4 8 4.44772 8 5C8 5.55228 8.44772 6 9 6H13.5C14.0523 6 14.5 5.55228 14.5 5C14.5 4.44772 14.0523 4 13.5 4Z" fill="white"/>
<path d="M13.5 8H9C8.44772 8 8 8.44772 8 9C8 9.55228 8.44772 10 9 10H13.5C14.0523 10 14.5 9.55228 14.5 9C14.5 8.44772 14.0523 8 13.5 8Z" fill="white"/>
<path d="M13.5 12H9C8.44772 12 8 12.4477 8 13C8 13.5523 8.44772 14 9 14H13.5C14.0523 14 14.5 13.5523 14.5 13C14.5 12.4477 14.0523 12 13.5 12Z" fill="white"/>
<path d="M4 13C4 13.5523 4.44772 14 5 14C5.55228 14 6 13.5523 6 13C6 12.4477 5.55228 12 5 12C4.44772 12 4 12.4477 4 13Z" fill="white"/>
<path d="M4 9C4 9.55228 4.44772 10 5 10C5.55228 10 6 9.55228 6 9C6 8.44772 5.55228 8 5 8C4.44772 8 4 8.44772 4 9Z" fill="white"/>
<path d="M4 5C4 5.55228 4.44772 6 5 6C5.55228 6 6 5.55228 6 5C6 4.44772 5.55228 4 5 4C4.44772 4 4 4.44772 4 5Z" fill="white"/>
<path d="M16.5 13C16.5 13.5523 16.9477 14 17.5 14C18.0523 14 18.5 13.5523 18.5 13C18.5 12.4477 18.0523 12 17.5 12C16.9477 12 16.5 12.4477 16.5 13Z" fill="white"/>
<path d="M16.5 9C16.5 9.55228 16.9477 10 17.5 10C18.0523 10 18.5 9.55228 18.5 9C18.5 8.44772 18.0523 8 17.5 8C16.9477 8 16.5 8.44772 16.5 9Z" fill="white"/>
<path d="M16.5 5C16.5 5.55228 16.9477 6 17.5 6C18.0523 6 18.5 5.55228 18.5 5C18.5 4.44772 18.0523 4 17.5 4C16.9477 4 16.5 4.44772 16.5 5Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8 12.5C8 13.324 8.94076 13.7944 9.6 13.3L12.9333 10.8C13.4667 10.4 13.4667 9.6 12.9333 9.2L9.6 6.7C8.94076 6.20557 8 6.67595 8 7.5V12.5ZM10 0C4.48 0 0 4.48 0 10C0 15.52 4.48 20 10 20C15.52 20 20 15.52 20 10C20 4.48 15.52 0 10 0ZM10 18C5.59 18 2 14.41 2 10C2 5.59 5.59 2 10 2C14.41 2 18 5.59 18 10C18 14.41 14.41 18 10 18Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 451 B

@ -0,0 +1,29 @@
<svg width="144" height="31" viewBox="0 0 144 31" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_5_10489)">
<path d="M134.436 0.0644531H61.5517C56.2735 0.0644531 51.9946 4.3433 51.9946 9.62152V21.1889C51.9946 26.4671 56.2735 30.7459 61.5517 30.7459H134.436C139.715 30.7459 143.994 26.4671 143.994 21.1889V9.62152C143.994 4.3433 139.715 0.0644531 134.436 0.0644531Z" fill="#0B2E4D"/>
<path d="M65.5072 0H9.84982C4.40992 0 0 4.40992 0 9.84982V20.8967C0 26.3366 4.40992 30.7465 9.84982 30.7465H65.5072C70.9471 30.7465 75.357 26.3366 75.357 20.8967V9.84982C75.357 4.40992 70.9471 0 65.5072 0Z" fill="url(#paint0_linear_5_10489)"/>
<path d="M90.2752 16.9092C90.2752 20.8777 88.4145 22.8685 84.6932 22.8685C81.2711 22.8685 79.5601 21.1185 79.5601 17.6118C79.5601 16.675 79.6967 15.6926 79.9699 14.6582L83.5026 14.0271C83.1513 15.1656 82.9756 16.2521 82.9756 17.2865C82.9756 19.2318 83.6197 20.2011 84.9144 20.2011C85.604 20.2011 86.0919 19.9864 86.3847 19.5506C86.6904 19.0886 86.8401 18.2884 86.8401 17.1434V10.2667H90.2687V16.9092H90.2752ZM83.3855 12.7715C83.0667 12.4527 82.9106 12.0689 82.9106 11.62C82.9106 11.171 83.0667 10.7872 83.3855 10.4684C83.6978 10.1561 84.0751 10 84.5305 10C84.9859 10 85.3633 10.1561 85.682 10.4684C86.0008 10.7872 86.157 11.171 86.157 11.62C86.157 12.0689 86.0008 12.4527 85.682 12.7715C85.3633 13.0838 84.9794 13.2399 84.5305 13.2399C84.0816 13.2399 83.6978 13.0838 83.3855 12.7715Z" fill="white"/>
<path d="M92.5195 6.03809H95.9351V18.7245H92.5195V6.03809Z" fill="white"/>
<path d="M97.939 23.0445L98.1146 20.3186C98.9409 20.4747 99.8452 20.5528 100.828 20.5528C102.578 20.5528 103.456 19.9477 103.456 18.7311V18.7116H102.63C99.3442 18.7116 97.6982 17.3389 97.6982 14.5934C97.6982 13.2858 98.0886 12.2188 98.8758 11.3926C99.7736 10.4557 101.075 9.9873 102.779 9.9873C104.347 9.9873 105.681 10.1955 106.767 10.6119V16.0507H108.205V18.7116H106.748C106.657 20.1364 106.104 21.2294 105.095 21.9971C104.041 22.8038 102.532 23.2071 100.561 23.2071C99.6955 23.2071 98.8172 23.1486 97.9325 23.0315L97.939 23.0445ZM103.456 12.8694C103.189 12.7263 102.857 12.6612 102.454 12.6612C102.018 12.6612 101.68 12.8238 101.426 13.1556C101.198 13.4614 101.081 13.8388 101.081 14.3007C101.081 15.4782 101.699 16.0637 102.935 16.0637H103.456V12.8759V12.8694Z" fill="white"/>
<path d="M107.938 16.0635H111.699V18.7244H107.938V16.0635Z" fill="white"/>
<path d="M111.334 16.0635H115.095V18.7244H111.334V16.0635Z" fill="white"/>
<path d="M114.737 16.0635H118.497V18.7244H114.737V16.0635Z" fill="white"/>
<path d="M118.133 16.0635H121.894V18.7244H118.133V16.0635Z" fill="white"/>
<path d="M121.536 16.0633H124.593V10.2666H128.009V18.7242H121.536V16.0633ZM124.171 22.5561C123.871 22.8684 123.5 23.0245 123.065 23.0245C122.629 23.0245 122.271 22.8684 121.978 22.5561C121.685 22.2438 121.536 21.8665 121.536 21.4241C121.536 20.9817 121.679 20.6174 121.978 20.3116C122.277 19.9993 122.642 19.8432 123.065 19.8432C123.487 19.8432 123.871 19.9993 124.171 20.3116C124.463 20.6239 124.613 20.9947 124.613 21.4241C124.613 21.8535 124.463 22.2438 124.171 22.5561ZM127.625 22.5561C127.332 22.8684 126.968 23.0245 126.539 23.0245C126.109 23.0245 125.738 22.8684 125.439 22.5561C125.14 22.2438 124.99 21.8665 124.99 21.4241C124.99 20.9817 125.14 20.6174 125.439 20.3116C125.738 19.9993 126.103 19.8432 126.539 19.8432C126.975 19.8432 127.332 19.9993 127.625 20.3116C127.918 20.6239 128.068 20.9947 128.068 21.4241C128.068 21.8535 127.918 22.2438 127.625 22.5561Z" fill="white"/>
<path d="M129.674 16.0636H134.372C134.28 15.1592 133.91 14.4046 133.266 13.7865C132.589 13.1359 131.678 12.7196 130.553 12.5309L131.346 9.71387C135.595 10.5661 137.716 13.0318 137.716 17.111V18.7244H129.674V16.0636Z" fill="white"/>
<path d="M15.3145 22.2177C14.0849 23.2717 12.53 23.7986 10.6433 23.7986C9.11443 23.7986 7.81326 23.662 6.72679 23.3953L7.10413 19.2445C8.28819 19.7455 9.49827 19.9992 10.7344 19.9992C11.7493 19.9992 12.2567 19.6284 12.2567 18.8802C12.2567 18.3858 11.951 17.9824 11.3329 17.6766C11.1247 17.5725 10.8124 17.4359 10.3961 17.2733C9.8626 17.0846 9.49827 16.948 9.3096 16.8634C7.33834 16.0307 6.35596 14.5864 6.35596 12.524C6.35596 10.9366 6.93498 9.68098 8.08651 8.75715C9.26406 7.82682 10.8775 7.3584 12.9399 7.3584C13.9743 7.3584 14.9437 7.46249 15.848 7.66417L15.4836 11.431C14.5923 11.1188 13.688 10.9626 12.7837 10.9626C11.6322 10.9626 11.0532 11.3204 11.0532 12.0296C11.0532 12.4525 11.372 12.8168 12.003 13.1421C12.2112 13.2397 12.53 13.3828 12.9724 13.565L14.0914 14.0204C16.1212 14.9442 17.1361 16.369 17.1361 18.3012C17.1361 19.8756 16.5311 21.1768 15.3145 22.2177Z" fill="white"/>
<path d="M28.4238 17.0324C27.2267 18.2229 25.5482 18.815 23.3883 18.815C23.1671 18.815 22.9719 18.8085 22.8028 18.7889V23.4796H18.4438V7.67049C20.0052 7.58591 21.7033 7.54688 23.5249 7.54688C27.8903 7.54688 30.0698 9.29694 30.0698 12.7971C30.0698 14.5276 29.5233 15.9394 28.4238 17.0324ZM23.4143 11.1186C23.2451 11.1186 23.0435 11.1251 22.8093 11.1446V15.1522C23.0305 15.1977 23.2907 15.2238 23.59 15.2238C24.9171 15.2238 25.5807 14.5341 25.5807 13.1614C25.5807 11.7887 24.8586 11.1251 23.4208 11.1251L23.4143 11.1186Z" fill="white"/>
<path d="M43.8817 21.4829C42.3659 23.0378 40.4662 23.8185 38.1956 23.8185C35.7559 23.8185 33.9018 23.1158 32.6331 21.7041C31.4035 20.3378 30.792 18.3991 30.792 15.8944C30.792 13.3896 31.5792 11.2817 33.1471 9.66828C34.663 8.1199 36.5562 7.3457 38.8202 7.3457C41.2599 7.3457 43.1205 8.04833 44.3957 9.4601C45.6253 10.8198 46.2368 12.752 46.2368 15.2633C46.2368 17.7745 45.4496 19.8889 43.8817 21.4894V21.4829ZM38.5469 11.3208C37.5841 11.3208 36.8294 11.6786 36.2699 12.3942C35.7104 13.1294 35.4372 14.1508 35.4372 15.4715C35.4372 18.3536 36.4521 19.7979 38.4819 19.7979C39.4447 19.7979 40.1994 19.4465 40.7459 18.7439C41.3119 18.0218 41.5917 17.0004 41.5917 15.6797C41.5917 12.7716 40.5768 11.3208 38.5469 11.3208Z" fill="white"/>
<path d="M55.1305 23.4862L52.2028 17.722H51.9946V23.4862H47.6357V7.67049C48.8653 7.58591 50.6675 7.54688 53.0486 7.54688C57.2383 7.54688 59.3332 9.19285 59.3332 12.4913C59.3332 13.4542 59.0795 14.3325 58.5785 15.1262C58.0581 15.9394 57.3685 16.5119 56.4967 16.8307C56.9651 17.5333 57.3424 18.1449 57.6417 18.6588L60.4327 23.4862H55.137H55.1305ZM52.8209 11.1381C52.4631 11.1381 52.1898 11.1511 51.9946 11.1836V14.5797C52.2158 14.6122 52.4566 14.6252 52.7168 14.6252C54.07 14.6252 54.7401 14.0202 54.7401 12.8166C54.7401 11.6976 54.1025 11.1316 52.8209 11.1316V11.1381Z" fill="white"/>
<path d="M67.719 11.4186V23.4869H63.3601V11.4186H59.938V7.67773H71.141V11.4186H67.719Z" fill="white"/>
</g>
<defs>
<linearGradient id="paint0_linear_5_10489" x1="0" y1="15.3733" x2="75.357" y2="15.3733" gradientUnits="userSpaceOnUse">
<stop stop-color="#005D6B"/>
<stop offset="1" stop-color="#1DB9AB"/>
</linearGradient>
<clipPath id="clip0_5_10489">
<rect width="144" height="30.7465" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 6.7 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 21 KiB

@ -54,14 +54,21 @@
id="ze-snippet"
src="https://static.zdassets.com/ekr/snippet.js?key=2f84e9fe-830c-42bf-afa4-32c90d7c5f7b"
></script>
<!-- Matomo Tag Manager -->
<!-- Matomo -->
<script>
var _mtm = window._mtm = window._mtm || [];
_mtm.push({'mtm.startTime': (new Date().getTime()), 'event': 'mtm.Start'});
var _paq = window._paq = window._paq || [];
/* tracker methods like "setCustomDimension" should be called before "trackPageView" */
_paq.push(['trackPageView']);
_paq.push(['enableLinkTracking']);
(function() {
var u="//matomo.insports.tv/";
_paq.push(['setTrackerUrl', u+'matomo.php']);
_paq.push(['setSiteId', '1']);
var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
g.async=true; g.src='https://matomo.instat.tv/js/container_g9SlNJe0.js'; s.parentNode.insertBefore(g,s);
g.async=true; g.src=u+'matomo.js'; s.parentNode.insertBefore(g,s);
})();
</script>
<!-- End Matomo Tag Manager -->
<!-- End Matomo Code -->
<% } %>
<!-- End of tv-instat Zendesk Widget script -->
<!-- Start of ChromeCast script -->

@ -1,9 +0,0 @@
#!/bin/bash
branch=$1
composefile=$2
cd /home/ubuntu/ott-auth
docker-compose -f $composefile down
docker-compose -f $composefile up -d
echo "[>] Deployment done."

@ -1,6 +1,7 @@
import {
memo,
RefObject,
useEffect,
} from 'react'
import styled from 'styled-components/macro'
@ -15,10 +16,11 @@ const PipWrapper = styled.div`
`
type PipProps = {
isPlaying: boolean,
videoRef: RefObject<HTMLVideoElement>,
}
export const PiP = memo(({ videoRef }: PipProps) => {
export const PiP = memo(({ isPlaying, videoRef }: PipProps) => {
const togglePip = async () => {
try {
if (
@ -33,6 +35,20 @@ export const PiP = memo(({ videoRef }: PipProps) => {
}
}
useEffect(() => {
window.addEventListener('visibilitychange', async () => {
if (
document.hidden === true
&& document.pictureInPictureEnabled
&& videoRef.current !== document.pictureInPictureElement
&& videoRef.current?.hidden === false
&& isPlaying
) {
await videoRef.current?.requestPictureInPicture()
}
})
}, [videoRef, isPlaying])
return (
<PipWrapper>
<Icon refIcon='PiP' onClick={togglePip} />

@ -1,6 +1,7 @@
import { Icon } from 'features/Icon'
import { add } from 'date-fns'
import { isIOS } from 'config'
import {
ScBannerWrap,
@ -38,7 +39,9 @@ export const SmartBanner = ({ setIsOpenDownload }: SmartBannerProps) => (
<ScDescribe>inSports the Home of Sports Streaming</ScDescribe>
</ScTitleWrap>
<a
href='https://play.google.com/store/apps/details?id=instat.ott'
href={isIOS
? 'https://apps.apple.com/es/app/insports-tv/id1643858776'
: 'https://play.google.com/store/apps/details?id=instat.ott'}
target='_blank'
rel='noreferrer'
onClick={() => {

@ -52,13 +52,14 @@ export const SportIcon = ({
sport,
}: Props) => {
const sportType = getSportLexic(sport)
const IconSport = sportIcons[sportType].icon
const IconSport = sportIcons[sportType]?.icon
return (
<IconWrap>
{IconSport && (
<Icon
refIcon={IconSport}
color={fill || sportIcons[sportType].color}
color={fill || sportIcons[sportType]?.color}
size={size}
styles={{
alignItems: 'center',
@ -67,6 +68,7 @@ export const SportIcon = ({
justifyContent: 'center',
}}
/>
)}
</IconWrap>
)
}

@ -4,6 +4,8 @@ import { facr } from './facr'
import { instat } from './instat'
import { lff } from './lff'
import { insports } from './insports'
import { india } from './india'
import { tunis } from './tunis'
export const currentClient = process.env.REACT_APP_CLIENT || 'insports'
@ -11,12 +13,16 @@ export const isLffClient = currentClient === 'lff'
export const isInSportsClient = currentClient === 'insports'
export const isInstatClient = currentClient === 'instat'
export const isFacrClient = currentClient === 'facr'
export const isIndiaClient = currentClient === 'india'
export const isTunisClient = currentClient === 'tunis'
const clients = {
facr,
india,
insports,
instat,
lff,
tunis,
}
export const client: ClientConfig = clients[currentClient]

@ -0,0 +1,16 @@
import {
ClientConfig,
ClientIds,
ClientNames,
} from './types'
import { insports } from './insports'
export const india: ClientConfig = {
...insports,
about_the_project: 'https://prsolution.pro',
auth: {
clientId: ClientIds.India,
},
name: ClientNames.India,
}

@ -15,6 +15,7 @@ export const insports: ClientConfig = {
name: ClientNames.Insports,
privacyLink: '/privacy-policy-and-statement?client_id=insports-ott-web',
showSearch: true,
showSmartBanner: true,
styles: {
background: 'background-image: url(/images/Checker.png);',
logo: 'insports-logo.svg',

@ -0,0 +1,57 @@
import { css } from 'styled-components/macro'
import {
ClientConfig,
ClientIds,
ClientNames,
} from './types'
const randomHash = () => (
(Math.random() ** Math.random()) * 9999999999999999
)
export const tunis: ClientConfig = {
auth: {
clientId: ClientIds.Tunis,
metaDataUrlParams: `?hash=${randomHash()}`,
},
defaultLanguage: 'fr',
description: 'Live sports streaming platform. All matches playing under the auspices of Czech Republic FA. Access to full matches, various player playlists, and highlights. Free access in the Czech Republic. Available across all devices',
disabledPreferences: false,
name: ClientNames.Tunis,
privacyLink: '/privacy-policy-and-statement',
showSearch: false,
styles: {
background: '',
homePageHeader: css`
background: radial-gradient(
160.34% 257.27% at -7.45% 162.22%,
#2AB7AA 3.27%,
#02505C 43.69%, #0B2E4D 100%);
`,
logo: 'tunis-logo.svg',
logoHeight: 6.3,
logoLeft: 1.1,
logoTop: 1.74,
logoWidth: 8.25,
matchLogoHeight: 3.4,
matchLogoTopMargin: 0.9,
matchLogoWidth: 4.5,
matchPageMobileHeaderLogo: css`
width: 35px;
height: 25px;
top: 2px;
`,
mobileHeaderLogo: css`
width: 48px;
height: 37px;
`,
userAccountLogo: css`
width: 4.56rem;
height: 3.488rem;
`,
},
termsLink: '/terms-and-conditions?client_id=facr-ott-web',
title: 'FACR.TV - The home of Czech football streaming',
userAccountLinksDisabled: true,
}

@ -6,19 +6,24 @@ type StyledCss = ReturnType<typeof css>
export enum ClientIds {
Facr = 'facr-ott-web',
India = 'india-ott-web',
Insports = 'insports-ott-web',
Instat = 'ott-web',
Lff = 'lff-ott-web',
Tunis = 'tunis-ott-web',
}
export enum ClientNames {
Facr = 'facr',
India = 'india',
Insports = 'insports',
Instat = 'instat',
Lff = 'lff',
Tunis = 'tunis',
}
export type ClientConfig = {
about_the_project?: string,
auth: {
clientId: ClientIds,
metaDataUrlParams?: string,
@ -30,6 +35,7 @@ export type ClientConfig = {
privacyLink: string,
requests?: Record<ProcedureName, RequestParameters>,
showSearch?: boolean,
showSmartBanner?: boolean,
styles: {
background?: string,
homePageHeader?: StyledCss,

@ -16,7 +16,7 @@ export const isProduction = ENV === 'production' || ENV === 'preproduction'
export const stageENV = process.env.REACT_APP_STAGE || 'test'
export const STRIPE_PUBLIC_KEY = process.env.REACT_APP_STRIPE_PK || 'pk_test_51J5TEYEDSxVnTgDWhKLstuDAhx9XmGJmj2awyZ1HghpWdU46MhXqbQt1PyW9XsRlES5JFyuQWbPRjoSsiW3wvXOH00KMirJEGZ'
export const STRIPE_PUBLIC_KEY = process.env.REACT_APP_STRIPE_PK || 'pk_test_51J5TEYEDSxVnTgDWyF63HykCAwKKObIdYCKiCwotte7xvfPw0VhmZiQKzYJIgZ3tCVvQ57JNpGYN7YbxR4JckYUB00HeDWE4YR'
export const GOOGLE_CLIENT_ID = process.env.REACT_APP_GOOGLE_CLIENT_ID || '1043133237396-kebgih109kro71b5c7c8qphtgjbd2gdk.apps.googleusercontent.com'
export const FACEBOOK_CLIENT_ID = process.env.REACT_APP_FACEBOOK_CLIENT_ID || '798254931203361'

@ -9,8 +9,12 @@ const matchPopupLexics = {
apply: 13491,
choose_fav_team: 19776,
commentators: 15424,
current_stats: 19592,
display_all_stats: 19932,
display_stats_according_to_video: 19931,
episode_duration: 13410,
events: 1020,
final_stats: 19591,
from_end_match: 15396,
from_price: 3992,
from_start_match: 15395,
@ -22,6 +26,7 @@ const matchPopupLexics = {
match_interviews: 13031,
match_settings: 13490,
no_data: 15397,
others: 19902,
players_episodes: 13398,
playlist_format: 13406,
playlist_format_all_actions: 13408,
@ -31,6 +36,7 @@ const matchPopupLexics = {
sec_before: 13411,
selected_player_actions: 13413,
started_streaming_at: 16042,
stats: 18179,
streamed_live_on: 16043,
video: 1017,
views: 13440,
@ -158,6 +164,7 @@ export const indexLexics = {
no_match_access_body: 13419,
no_match_access_title: 13418,
player: 14975,
players: 164,
players_video: 13032,
privacy_policy_and_statement: 15404,
round_highilights: 13050,

@ -5,20 +5,27 @@ import { ENV, isProduction } from './env'
export const APIS = {
preproduction: {
api: 'https://api.insports.tv',
auth: 'https://api.auth.insports.tv',
auth: 'https://auth.insports.tv',
},
production: {
api: 'https://api.insports.tv',
auth: 'https://api.auth.insports.tv',
auth: 'https://auth.insports.tv',
},
staging: {
api: 'https://api.test.insports.tv',
auth: 'https://api.auth.test.insports.tv',
auth: 'https://auth.test.insports.tv',
},
}
const VIEWS_APIS = {
preproduction: 'https://views.insports.tv',
production: 'https://views.insports.tv',
staging: 'https://views.test.insports.tv',
}
const env = isProduction ? ENV : readSelectedApi() ?? ENV
export const VIEWS_API = VIEWS_APIS[env]
export const AUTH_SERVICE = APIS[env].auth
export const API_ROOT = APIS[env].api
export const DATA_URL = `${API_ROOT}/data`

@ -1,3 +1,5 @@
export const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent)
export const isAndroid = /Android/.test(navigator.userAgent)
export const isMobileDevice = /iPhone|Android/.test(navigator.userAgent)

@ -11,6 +11,8 @@ import {
HeaderTitle,
} from 'features/AuthServiceApp/components/RegisterPopup/styled'
import { client } from '../../config/clients/index'
export const Modal = styled(BaseModal)`
${ModalWindow} {
min-height: 220px;
@ -59,6 +61,7 @@ export const ScApplyButton = styled(ApplyButton)`
font-size: 14px;
`
: ''};
${client.styles.popupApplyButton}
`
export const ScLink = styled.a`

@ -25,11 +25,6 @@ export const RegisterPopup = (props: Props) => {
isModalOpen,
} = props
// const handleNewConfirmation = () => {
// // TODO дописать логику для отправки доп. письма, может понадобится, когда допишут бэк
// // console.log('send new confirmation')
// }
return (
<Modal isOpen={isModalOpen} withCloseButton={false}>
<Wrapper>
@ -53,9 +48,6 @@ export const RegisterPopup = (props: Props) => {
</Body>
<Footer>
<ApplyButton onClick={() => handleModalClose()}>Ok</ApplyButton>
{/* <SendConfirmationButton onClick={handleNewConfirmation}>
<T9n t='send_new_email' />
</SendConfirmationButton> */}
</Footer>
</Wrapper>
</Modal>

@ -7,7 +7,9 @@ import { ModalWindow } from 'features/Modal/styled'
import { Modal as BaseModal } from 'features/Modal'
import { Header as BaseHeader } from 'features/PopupComponents'
import { ButtonSolid, ButtonOutline } from 'features/Common'
import { ButtonSolid } from 'features/Common'
import { client } from '../../config/clients'
export const Modal = styled(BaseModal)`
background-color: rgba(0, 0, 0, 0.7);
@ -139,26 +141,9 @@ export const ApplyButton = styled(ButtonSolid)`
}
`
: ''};
${client.styles.popupApplyButton}
`
export const SendConfirmationButton = styled(ButtonOutline)`
width: 100%;
height: 50px;
border-radius: 5px;
font-weight: 500;
font-size: 20px;
${isMobileDevice
? css`
@media ${devices.mobile}{
width: 100%;
}
@media (orientation: landscape){
width: 290px;
}
`
: ''};
`
export const Text = styled.span`
margin-bottom: 20px;
`

@ -4,12 +4,16 @@ import { facr } from './facr'
import { insports } from './insports'
import { instat } from './instat'
import { lff } from './lff'
import { india } from './india'
import { tunis } from './tunis'
const clients = {
[ClientIds.Facr]: facr,
[ClientIds.Instat]: instat,
[ClientIds.Lff]: lff,
[ClientIds.Insports]: insports,
[ClientIds.India]: india,
[ClientIds.Tunis]: tunis,
}
const params = new URLSearchParams(window.location.search)

@ -0,0 +1,9 @@
import { insports as platformInsports } from 'config/clients/insports'
import type { ClientConfig } from './types'
import { insports } from './insports'
export const india: ClientConfig = {
...platformInsports,
...insports,
}

@ -0,0 +1,67 @@
import styled, { css } from 'styled-components/macro'
import { tunis as platformTunis } from 'config/clients/tunis'
import { isMobileDevice } from 'config/userAgent'
import type { ClientConfig } from './types'
const Background = styled.div`
position: relative;
width: 100%;
height: 100vh;
display: flex;
justify-content: center;
background: linear-gradient(0deg, rgba(2, 46, 48, 0.3),
rgba(2, 46, 48, 0.3)),
radial-gradient(152.89% 271.81% at 0% 96.71%, #2AB7AA 3.27%, #02505C 43.69%, #0B2E4D 100%);
`
export const tunis: ClientConfig = {
...platformTunis,
background: Background,
styles: {
centerBlock: css`
margin-top: 9.15rem;
${isMobileDevice ? css`
margin-top: 107px;
@media screen and (orientation: landscape) {
width: 290px;
margin: auto;
}
` : ''};
`,
input: css`
background-color: transparent;
:not(:last-of-type) {
border-color: ${({ theme }) => theme.colors.white};
}
`,
inputGroup: css`
border: 1px solid ${({ theme }) => theme.colors.white};
`,
loader: css`
color: #0B2E4D;
`,
logo: css`
background-image: url(/images/tunis_auth_logo.svg);
width: 200px;
height: 178px;
margin-bottom: 1.82rem;
${isMobileDevice ? css`
margin-bottom: 20px;
width: 130px;
height: 100px;
` : ''}
`,
popupApplyButton: css`
background-color: #0E8F84;
color: ${({ theme }) => theme.colors.white};
`,
popupLoader: '#FFFFFF',
submitButton: css`
background-color: ${({ theme }) => theme.colors.white};
color: #0B2E4D;
`,
},
}

@ -12,6 +12,7 @@ export type ClientConfig = {
background: FC<{ children: ReactNode }>,
defaultLanguage: string,
description: string,
isHideSelectLanguages?: boolean,
name: ClientNames,
privacyLink: string,
styles: {

@ -23,6 +23,10 @@ export const getClientNameByRedirectUri = () => {
switch (client.name) {
case ClientNames.Lff:
return 'lff.instat'
case ClientNames.India:
return 'india.insports'
case ClientNames.Tunis:
return ClientNames.Tunis
case ClientNames.Facr:
return ClientNames.Facr
case ClientNames.Instat:
@ -35,7 +39,13 @@ export const getClientNameByRedirectUri = () => {
const redirectUrl = () => {
const clientName = getClientNameByRedirectUri()
switch (true) {
case (process.env.NODE_ENV === 'development' || client.name === 'lff' || client.name === 'facr'):
case (
process.env.NODE_ENV === 'development'
|| client.name === 'lff'
|| client.name === 'facr'
|| client.name === 'india'
|| client.name === 'tunis'
):
return `${window.origin}/redirect`
case (ENV === 'staging' || ENV === 'preproduction'):
return `https://${stageENV}.insports.tv/redirect`

@ -39,6 +39,8 @@ export const CompanyInfo = ({
</CompanyInfoText>
</Fragment>
)
case ClientNames.Tunis:
return ''
case ClientNames.Lff:
return (
<Fragment>

@ -1,7 +1,7 @@
import { useRecoilValue } from 'recoil'
import { isIOS } from 'config/userAgent'
import { isLffClient } from 'config/clients'
import { isAndroid, isIOS } from 'config/userAgent'
import { client, isLffClient } from 'config/clients'
import { HeaderMenu } from 'features/HeaderMenu'
import { DateFilter } from 'features/HeaderFilters'
@ -10,6 +10,8 @@ import { SportsFilter } from 'features/SportsFilter'
import { isSportFilterShownAtom } from 'features/HomePage/Atoms/HomePageAtoms'
import { SmartBanner } from 'components/SmartBanner'
import { UserInfo } from 'requests'
import {
HeaderStyled,
ScoreSwitchWrapper,
@ -19,16 +21,22 @@ import {
type HeaderBannerProps = {
isOpenDownload: boolean,
setIsOpenDownload: (open: boolean) => void,
userInfo?: UserInfo,
}
export const HeaderMobile = ({ isOpenDownload, setIsOpenDownload }: HeaderBannerProps) => {
export const HeaderMobile = ({
isOpenDownload,
setIsOpenDownload,
userInfo,
}: HeaderBannerProps) => {
const isSportFilterShown = useRecoilValue(isSportFilterShownAtom)
return (
<>
{
isOpenDownload
&& !isIOS
&& (isAndroid || (isIOS && userInfo?.has_subscription))
&& client.showSmartBanner
&& <SmartBanner setIsOpenDownload={setIsOpenDownload} />
}
<HeaderStyled>

@ -34,7 +34,7 @@ const getTimezoneOffset = (date: Date) => {
const getDate = (date: Date) => format(date, 'yyyy-MM-dd')
export const useHomePage = () => {
const { user } = useAuthStore()
const { user, userInfo } = useAuthStore()
const { selectedDate } = useHeaderFiltersStore()
const [isOpenDownload, setIsOpenDownload] = useState(false)
const [isShowConfirmPopup, setIsShowConfirmPopup] = useState(false)
@ -54,6 +54,7 @@ export const useHomePage = () => {
})()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
useEffect(() => {
const dateLastOpenSmartBanner = localStorage.getItem('dateLastOpenSmartBanner')
if (!dateLastOpenSmartBanner
@ -85,5 +86,6 @@ export const useHomePage = () => {
isOpenDownload,
isShowConfirmPopup,
setIsOpenDownload,
userInfo,
}
}

@ -29,6 +29,7 @@ const Home = () => {
isOpenDownload,
isShowConfirmPopup,
setIsOpenDownload,
userInfo,
} = useHomePage()
return (
@ -37,6 +38,7 @@ const Home = () => {
<HeaderMobile
isOpenDownload={isOpenDownload}
setIsOpenDownload={setIsOpenDownload}
userInfo={userInfo}
/>
) : (
<Header />

@ -17,7 +17,11 @@ import { useMatchPageStore } from '../../store'
export const FinishedMatch = () => {
const [circleAnimation, setCircleAnimation] = useState<TCircleAnimation>(initialCircleAnimation)
const { isOpenPopup, profile } = useMatchPageStore()
const {
isOpenFiltersPopup,
profile,
setPlayingProgress,
} = useMatchPageStore()
const {
chapters,
closeSettingsPopup,
@ -48,9 +52,10 @@ export const FinishedMatch = () => {
<Fragment>
<MultiSourcePlayer
setCircleAnimation={setCircleAnimation}
isOpenPopup={isOpenPopup}
isOpenPopup={isOpenFiltersPopup}
chapters={chapters}
onPlayingChange={onPlayingChange}
onPlayerProgressChange={setPlayingProgress}
profile={profile}
/>
<MatchDescription />

@ -21,6 +21,7 @@ export const useLiveMatch = () => {
profile,
selectedPlaylist,
setFullMatchPlaylistDuration,
setPlayingProgress,
} = useMatchPageStore()
const { profileId: matchId, sportType } = usePageParams()
const resume = useResumeUrlParam()
@ -45,7 +46,7 @@ export const useLiveMatch = () => {
} = usePlaylistLogger()
const {
onPlayerProgressChange,
onPlayerProgressChange: playerProgressChange,
onPlayingChange: notifyProgressLogger,
} = usePlayerProgressReporter()
@ -66,6 +67,11 @@ export const useLiveMatch = () => {
handlePlaylistClick(playlist, e)
}
const onPlayerProgressChange = (seconds: number, period = 0) => {
playerProgressChange(seconds, period)
setPlayingProgress(seconds * 1000)
}
return {
chapters,
isPlayFilterEpisodes,

@ -82,6 +82,7 @@ const MatchPageComponent = () => {
const sportName = history.location.pathname.split('/')[1]
history.push(`/${sportName}/tournaments/${profile.tournament.id}`)
}
return (
<PageWrapper isIOS={isIOS}>
<ProfileHeader color='#2B2A28' height={client.name === 'facr' ? 5 : 4.5} />

@ -41,7 +41,7 @@ export const useMatchPage = () => {
countOfFilters,
filters,
isEmptyFilters,
isOpen: isOpenPopup,
isOpen: isOpenFiltersPopup,
resetEvents,
resetPlayers,
toggle: togglePopup,
@ -66,9 +66,16 @@ export const useMatchPage = () => {
const {
events,
handlePlaylistClick,
isEmptyPlayersStats,
matchPlaylists,
playersData,
playersStats,
selectedPlaylist,
setFullMatchPlaylistDuration,
setPlayingProgress,
setStatsType,
statsType,
teamsStats,
} = useMatchData(matchProfile)
const profile = matchProfile
@ -160,8 +167,9 @@ export const useMatchPage = () => {
handlePlaylistClick,
hideProfileCard,
isEmptyFilters,
isEmptyPlayersStats,
isLiveMatch,
isOpenPopup,
isOpenFiltersPopup,
isPlayFilterEpisodes,
isStarted,
likeImage,
@ -170,6 +178,8 @@ export const useMatchPage = () => {
plaingOrder,
playEpisodes,
playNextEpisode,
playersData,
playersStats,
profile,
profileCardShown,
resetEvents,
@ -179,10 +189,14 @@ export const useMatchPage = () => {
setFullMatchPlaylistDuration,
setIsPlayinFiltersEpisodes,
setPlaingOrder,
setPlayingProgress,
setReversed,
setStatsType,
setUnreversed,
setWatchAllEpisodesTimer,
showProfileCard,
statsType,
teamsStats,
toggleActiveEvents,
toggleActivePlayers,
togglePopup,

@ -6,7 +6,7 @@ import {
import debounce from 'lodash/debounce'
import { MatchInfo } from 'requests/getMatchInfo'
import type { MatchInfo } from 'requests/getMatchInfo'
import { usePageParams } from 'hooks/usePageParams'
import { useInterval } from 'hooks/useInterval'
@ -16,6 +16,9 @@ import { useMatchPopupStore } from 'features/MatchPopup'
import { useMatchPlaylists } from './useMatchPlaylists'
import { useEvents } from './useEvents'
import { useTeamsStats } from './useTeamsStats'
import { useStatsTab } from './useStatsTab'
import { usePlayersStats } from './usePlayersStats'
const MATCH_DATA_POLL_INTERVAL = 60000
const MATCH_PLAYLISTS_DELAY = 5000
@ -24,6 +27,7 @@ export const useMatchData = (profile: MatchInfo) => {
const { profileId: matchId, sportType } = usePageParams()
const { chapters } = useMatchPopupStore()
const [matchDuration, setMatchDuration] = useState(0)
const [playingProgress, setPlayingProgress] = useState(0)
const {
fetchMatchPlaylists,
handlePlaylistClick,
@ -33,6 +37,21 @@ export const useMatchData = (profile: MatchInfo) => {
setSelectedPlaylist,
} = useMatchPlaylists(profile)
const { events, fetchMatchEvents } = useEvents()
const { setStatsType, statsType } = useStatsTab()
const { teamsStats } = useTeamsStats({
matchProfile: profile,
playingProgress,
statsType,
})
const {
isEmptyPlayersStats,
playersData,
playersStats,
} = usePlayersStats({
matchProfile: profile,
playingProgress,
statsType,
})
const fetchPlaylistsDebounced = useMemo(
() => debounce(fetchMatchPlaylists, MATCH_PLAYLISTS_DELAY),
@ -93,8 +112,15 @@ export const useMatchData = (profile: MatchInfo) => {
return {
events,
handlePlaylistClick,
isEmptyPlayersStats,
matchPlaylists,
playersData,
playersStats,
selectedPlaylist,
setFullMatchPlaylistDuration,
setPlayingProgress,
setStatsType,
statsType,
teamsStats,
}
}

@ -0,0 +1,159 @@
import {
useMemo,
useEffect,
useState,
} from 'react'
import throttle from 'lodash/throttle'
import isEmpty from 'lodash/isEmpty'
import every from 'lodash/every'
import find from 'lodash/find'
import type {
MatchInfo,
PlayersStats,
Player,
} from 'requests'
import { getPlayersStats, getMatchParticipants } from 'requests'
import { useObjectState, usePageParams } from 'hooks'
import { StatsType } from 'features/MatchSidePlaylists/components/TabStats/config'
const REQUEST_DELAY = 3000
const STATS_POLL_INTERVAL = 30000
type UsePlayersStatsArgs = {
matchProfile: MatchInfo,
playingProgress: number,
statsType: StatsType,
}
type PlayersData = {
team1: Array<Player>,
team2: Array<Player>,
}
export const usePlayersStats = ({
matchProfile,
playingProgress,
statsType,
}: UsePlayersStatsArgs) => {
const [playersStats, setPlayersStats] = useObjectState<Record<string, PlayersStats>>({})
const [playersData, setPlayersData] = useState<PlayersData>({ team1: [], team2: [] })
const {
profileId: matchId,
sportName,
sportType,
} = usePageParams()
const isCurrentStats = statsType === StatsType.CURRENT_STATS
const progressSec = Math.floor(playingProgress / 1000)
const isEmptyPlayersStats = (teamId: number) => (
isEmpty(playersStats[teamId])
|| every(playersStats[teamId], isEmpty)
|| isEmpty(playersData[matchProfile?.team1.id === teamId ? 'team1' : 'team2'])
)
const fetchPlayers = useMemo(() => throttle((second?: number) => {
if (!matchProfile?.team1.id || !matchProfile?.team1.id) return
try {
getMatchParticipants({
matchId,
second,
sportType,
}).then((data) => {
const team1Players = find(data, { team_id: matchProfile.team1.id })?.players || []
const team2Players = find(data, { team_id: matchProfile.team2.id })?.players || []
setPlayersData({
team1: team1Players,
team2: team2Players,
})
})
// eslint-disable-next-line no-empty
} catch (e) {}
}, REQUEST_DELAY), [
matchId,
matchProfile?.team1.id,
matchProfile?.team2.id,
sportType,
])
const fetchPlayersStats = useMemo(() => throttle((second?: number) => {
if (!sportName || !matchProfile?.team1.id || !matchProfile?.team2.id) return
try {
getPlayersStats({
matchId,
second,
sportName,
teamId: matchProfile.team1.id,
}).then((data) => setPlayersStats({ [matchProfile.team1.id]: data }))
getPlayersStats({
matchId,
second,
sportName,
teamId: matchProfile.team2.id,
}).then((data) => setPlayersStats({ [matchProfile?.team2.id]: data }))
// eslint-disable-next-line no-empty
} catch (e) {}
}, REQUEST_DELAY), [
matchId,
matchProfile?.team1.id,
matchProfile?.team2.id,
setPlayersStats,
sportName,
])
useEffect(() => {
let interval: NodeJS.Timeout
fetchPlayers()
if (!isCurrentStats) {
fetchPlayersStats()
}
if (matchProfile?.live) {
interval = setInterval(() => {
if (isCurrentStats) return
fetchPlayersStats()
fetchPlayers()
}, STATS_POLL_INTERVAL)
}
return () => clearInterval(interval)
}, [
fetchPlayersStats,
fetchPlayers,
isCurrentStats,
matchProfile?.live,
])
useEffect(() => {
if (isCurrentStats) {
fetchPlayersStats(progressSec)
fetchPlayers(progressSec)
}
}, [
fetchPlayersStats,
fetchPlayers,
progressSec,
isCurrentStats,
matchProfile?.live,
])
return {
isEmptyPlayersStats,
playersData,
playersStats,
}
}

@ -0,0 +1,12 @@
import { useState } from 'react'
import { StatsType } from 'features/MatchSidePlaylists/components/TabStats/config'
export const useStatsTab = () => {
const [statsType, setStatsType] = useState<StatsType>(StatsType.FINAL_STATS)
return {
setStatsType,
statsType,
}
}

@ -0,0 +1,78 @@
import {
useEffect,
useState,
useMemo,
} from 'react'
import throttle from 'lodash/throttle'
import type { MatchInfo } from 'requests'
import { getTeamsStats, TeamStatItem } from 'requests'
import { usePageParams } from 'hooks/usePageParams'
import { StatsType } from 'features/MatchSidePlaylists/components/TabStats/config'
const REQUEST_DELAY = 3000
const STATS_POLL_INTERVAL = 30000
type UseTeamsStatsArgs = {
matchProfile: MatchInfo,
playingProgress: number,
statsType: StatsType,
}
export const useTeamsStats = ({
matchProfile,
playingProgress,
statsType,
}: UseTeamsStatsArgs) => {
const [teamsStats, setTeamsStats] = useState<{
[teamId: string]: Array<TeamStatItem>,
}>({})
const { profileId: matchId, sportName } = usePageParams()
const progressSec = Math.floor(playingProgress / 1000)
const isCurrentStats = statsType === StatsType.CURRENT_STATS
const fetchTeamsStats = useMemo(() => throttle((second?: number) => {
if (!sportName) return
getTeamsStats({
matchId,
second,
sportName,
}).then(setTeamsStats)
}, REQUEST_DELAY), [matchId, sportName])
useEffect(() => {
let timer: ReturnType<typeof setInterval>
if (!isCurrentStats) {
fetchTeamsStats()
}
if (matchProfile?.live) {
timer = setInterval(() => {
if (isCurrentStats) return
fetchTeamsStats()
}, STATS_POLL_INTERVAL)
}
return () => clearInterval(timer)
}, [fetchTeamsStats, matchProfile?.live, isCurrentStats])
useEffect(() => {
if (isCurrentStats) {
fetchTeamsStats(progressSec)
}
}, [fetchTeamsStats, progressSec, isCurrentStats])
return {
statsType,
teamsStats,
}
}

@ -1,8 +1,8 @@
import { useMemo } from 'react'
import type { ForwardedRef } from 'react'
import { forwardRef } from 'react'
import styled, { css } from 'styled-components/macro'
import filter from 'lodash/filter'
import isEmpty from 'lodash/isEmpty'
import map from 'lodash/map'
@ -15,6 +15,8 @@ import { T9n } from 'features/T9n'
import { PlayButton } from '../PlayButton'
export const LIST_ITEM_INDENT = 12
type Props = {
live?: boolean,
onSelect?: (selectedMathPlaylist: PlaylistOption) => void,
@ -25,7 +27,7 @@ type Props = {
const List = styled.ul``
const Item = styled.li`
margin-bottom: 12px;
margin-bottom: ${LIST_ITEM_INDENT}px;
width: 100%;
height: 36px;
${isMobileDevice
@ -36,24 +38,17 @@ const Item = styled.li`
: ''};
`
export const MatchPlaylists = ({
export const MatchPlaylists = forwardRef(
({
live,
onSelect,
playlists,
selectedMathPlaylist,
}: Props) => {
const filteredPlayListByDuration = useMemo(() => (
filter(playlists, (playlist) => (
live
? Boolean(playlist.duration) || (playlist.id === 'full_game')
: Boolean(playlist.duration)
))
), [playlists, live])
return (
<List>
}: Props,
ref: ForwardedRef<HTMLUListElement>) => (
<List ref={ref}>
{
map(filteredPlayListByDuration, (playlist) => (
map(playlists, (playlist) => (
<Item key={playlist.id}>
<PlayButton
duration={playlist.duration}
@ -68,5 +63,5 @@ export const MatchPlaylists = ({
))
}
</List>
)
}
),
)

@ -23,13 +23,15 @@ import { VideoDate } from './components/VideoDate'
import { MatchesWrapper } from './styled'
type Props = {
additionalScrollHeight: number,
profile: MatchInfo,
tournamentData: TournamentData,
}
const formatDate = (date: Date) => format(date, 'yyyy-MM-dd')
export const TabVideo = ({
export const Matches = ({
additionalScrollHeight,
profile,
tournamentData,
}: Props) => {
@ -75,7 +77,9 @@ export const TabVideo = ({
const hasScroll = scrollHeight > clientHeight
setOverflow(hasScroll)
}, [ref, selectedDate])
}, [ref.current?.clientHeight, selectedDate])
if (tournamentData.matches.length <= 1) return null
return (
<Fragment>
@ -86,7 +90,11 @@ export const TabVideo = ({
profileDate={profileDate}
onDateClick={setSelectedDate}
/>
<MatchesWrapper ref={ref} hasScroll={overflow}>
<MatchesWrapper
ref={ref}
hasScroll={overflow}
additionalScrollHeight={additionalScrollHeight}
>
{
map(sortBy(matches, ({ live }) => !live), (match) => (
<MatchCard

@ -2,9 +2,14 @@ import styled, { css } from 'styled-components/macro'
import { customScrollbar } from 'features/Common'
import { isMobileDevice } from '../../../../config/userAgent'
export const MatchesWrapper = styled.div<{hasScroll?: boolean}>`
type MatchesWrapperProps = {
additionalScrollHeight: number,
hasScroll?: boolean,
}
export const MatchesWrapper = styled.div<MatchesWrapperProps>`
overflow-y: auto;
max-height: calc(100vh - 170px);
max-height: calc(100vh - 165px - ${({ additionalScrollHeight }) => additionalScrollHeight}px);
padding-right: ${({ hasScroll }) => (hasScroll ? '10px' : '')};
> * {

@ -15,12 +15,10 @@ import type {
} from 'features/MatchPage/types'
import { Name } from 'features/Name'
import { T9n } from 'features/T9n'
import { isEqual } from '../../helpers'
import { PlayButton } from '../PlayButton'
import { BlockTitle } from '../../styled'
import {
Wrapper,
List,
@ -58,9 +56,6 @@ export const PlayersPlaylists = ({
return (
<Wrapper>
<BlockTitle>
<T9n t='players_episodes' />
</BlockTitle>
<Tabs>
<Tab
active={selectedTeam === Teams.TEAM1}

@ -6,7 +6,6 @@ export const Wrapper = styled.div`
display: flex;
flex-direction: column;
align-items: center;
margin-top: 25px;
`
export const List = styled.ul`
@ -29,9 +28,8 @@ export const PlayerAvatar = styled(ProfileLogo)`
`
export const Tabs = styled.div`
display: flex;
margin-top: 4px;
margin-bottom: 8px;
margin-top: -10px;
margin-bottom: 6px;
`
type TabProps = {

@ -0,0 +1,7 @@
export const PARAM_COLUMN_WIDTH = 50
export const REQUEST_DELAY = 3000
export const STATS_POLL_INTERVAL = 30000
export const DISPLAYED_PARAMS_COLUMNS = 4
export const FIRST_COLUMN_WIDTH_DEFAULT = 100
export const SCROLLBAR_WIDTH = 8
export const CELL_WIDTH = PARAM_COLUMN_WIDTH

@ -0,0 +1,58 @@
import { useState } from 'react'
import type { SortCondition, PlayersTableProps } from '../types'
import { usePlayers } from './usePlayers'
import { useTable } from './useTable'
export const usePlayersTable = ({ teamId }: PlayersTableProps) => {
const [sortCondition, setSortCondition] = useState<SortCondition>({ dir: 'asc', paramId: null })
const {
getFullName,
getPlayerParams,
players,
} = usePlayers({ sortCondition, teamId })
const {
containerRef,
firstColumnWidth,
getDisplayedValue,
handleScroll,
handleSortClick,
isExpanded,
paramColumnWidth,
params,
showExpandButton,
showLeftArrow,
showRightArrow,
slideLeft,
slideRight,
tableWrapperRef,
toggleIsExpanded,
} = useTable({
setSortCondition,
teamId,
})
return {
containerRef,
firstColumnWidth,
getDisplayedValue,
getFullName,
getPlayerParams,
handleScroll,
handleSortClick,
isExpanded,
paramColumnWidth,
params,
players,
showExpandButton,
showLeftArrow,
showRightArrow,
slideLeft,
slideRight,
sortCondition,
tableWrapperRef,
toggleIsExpanded,
}
}

@ -0,0 +1,83 @@
import { useMemo, useCallback } from 'react'
import orderBy from 'lodash/orderBy'
import isNil from 'lodash/isNil'
import trim from 'lodash/trim'
import type { Player, PlayerParam } from 'requests'
import { useToggle } from 'hooks'
import { useMatchPageStore } from 'features/MatchPage/store'
import { useLexicsStore } from 'features/LexicsStore'
import type { SortCondition } from '../types'
type UsePlayersArgs = {
sortCondition: SortCondition,
teamId: number,
}
export const usePlayers = ({ sortCondition, teamId }: UsePlayersArgs) => {
const { isOpen: isExpanded, toggle: toggleIsExpanded } = useToggle()
const {
playersData,
playersStats,
profile: matchProfile,
} = useMatchPageStore()
const { suffix } = useLexicsStore()
const getPlayerParams = useCallback(
(playerId: number) => playersStats[teamId][playerId] || {},
[playersStats, teamId],
)
const getDisplayedValue = ({ val }: PlayerParam) => (isNil(val) ? '-' : val)
const getFullName = useCallback((player: Player) => (
trim(`${player[`firstname_${suffix}`]} ${player[`lastname_${suffix}`]}`)
), [suffix])
const getParamValue = useCallback((playerId: number, paramId: number) => {
const playerParams = getPlayerParams(playerId)
const { val } = playerParams[paramId] || {}
return val
}, [getPlayerParams])
const sortedPlayers = useMemo(() => {
const players = playersData[matchProfile?.team1.id === teamId ? 'team1' : 'team2']
return isNil(sortCondition.paramId)
? orderBy(players, getFullName)
: orderBy(
players,
[
(player) => {
const paramValue = getParamValue(player.id, sortCondition.paramId!)
return isNil(paramValue) ? -1 : paramValue
},
getFullName,
],
sortCondition.dir,
)
}, [
getFullName,
getParamValue,
playersData,
matchProfile?.team1.id,
sortCondition.dir,
sortCondition.paramId,
teamId,
])
return {
getDisplayedValue,
getFullName,
getPlayerParams,
isExpanded,
players: sortedPlayers,
toggleIsExpanded,
}
}

@ -0,0 +1,171 @@
import type {
SyntheticEvent,
Dispatch,
SetStateAction,
} from 'react'
import {
useRef,
useState,
useEffect,
useMemo,
} from 'react'
import size from 'lodash/size'
import isNil from 'lodash/isNil'
import reduce from 'lodash/reduce'
import forEach from 'lodash/forEach'
import values from 'lodash/values'
import round from 'lodash/round'
import map from 'lodash/map'
import { isMobileDevice } from 'config'
import type { PlayerParam, PlayersStats } from 'requests'
import { useToggle } from 'hooks'
import { useMatchPageStore } from 'features/MatchPage/store'
import { useLexicsConfig } from 'features/LexicsStore'
import type { SortCondition } from '../types'
import {
PARAM_COLUMN_WIDTH,
DISPLAYED_PARAMS_COLUMNS,
FIRST_COLUMN_WIDTH_DEFAULT,
SCROLLBAR_WIDTH,
} from '../config'
type UseTableArgs = {
setSortCondition: Dispatch<SetStateAction<SortCondition>>,
teamId: number,
}
type HeaderParam = Pick<PlayerParam, 'id' | 'lexica_short' | 'lexic'>
export const useTable = ({
setSortCondition,
teamId,
}: UseTableArgs) => {
const containerRef = useRef<HTMLDivElement>(null)
const tableWrapperRef = useRef<HTMLDivElement>(null)
const [showLeftArrow, setShowLeftArrow] = useState(false)
const [showRightArrow, setShowRightArrow] = useState(false)
const { isOpen: isExpanded, toggle: toggleIsExpanded } = useToggle()
const { playersStats } = useMatchPageStore()
const params = useMemo(() => (
reduce<PlayersStats, Record<string, HeaderParam>>(
playersStats[teamId],
(acc, curr) => {
forEach(values(curr), ({
id,
lexic,
lexica_short,
}) => {
acc[id] = acc[id] || {
id,
lexic,
lexica_short,
}
})
return acc
},
{},
)
), [playersStats, teamId])
const lexics = useMemo(() => (
reduce<HeaderParam, Array<number>>(
values(params),
(acc, { lexic, lexica_short }) => {
if (lexic) acc.push(lexic)
if (lexica_short) acc.push(lexica_short)
return acc
},
[],
)
// eslint-disable-next-line react-hooks/exhaustive-deps
), [map(params, 'id').sort().join('')])
useLexicsConfig(lexics)
const paramsCount = size(params)
const getParamColumnWidth = () => {
const rest = (
(containerRef.current?.clientWidth || 0) - FIRST_COLUMN_WIDTH_DEFAULT - SCROLLBAR_WIDTH
)
const desktopWith = PARAM_COLUMN_WIDTH
const mobileWidth = paramsCount < DISPLAYED_PARAMS_COLUMNS ? 0 : rest / DISPLAYED_PARAMS_COLUMNS
return isMobileDevice ? mobileWidth : desktopWith
}
const getFirstColumnWidth = () => {
if (isExpanded) return 0
return paramsCount < DISPLAYED_PARAMS_COLUMNS ? 0 : FIRST_COLUMN_WIDTH_DEFAULT
}
const paramColumnWidth = getParamColumnWidth()
const firstColumnWidth = getFirstColumnWidth()
const slideLeft = () => tableWrapperRef.current?.scrollBy(-paramColumnWidth, 0)
const slideRight = () => tableWrapperRef.current?.scrollBy(paramColumnWidth, 0)
const getDisplayedValue = ({ val }: PlayerParam) => (isNil(val) ? '-' : round(val, 2))
const handleScroll = (e: SyntheticEvent<HTMLDivElement>) => {
const {
clientWidth,
scrollLeft,
scrollWidth,
} = e.currentTarget
const scrollRight = scrollWidth - (scrollLeft + clientWidth)
setShowLeftArrow(scrollLeft > 0)
setShowRightArrow(scrollRight > 0)
}
const handleSortClick = (paramId: number) => () => {
setSortCondition((curr) => ({
dir: curr.dir === 'asc' || curr.paramId !== paramId ? 'desc' : 'asc',
paramId,
}))
}
useEffect(() => {
const {
clientWidth = 0,
scrollLeft = 0,
scrollWidth = 0,
} = tableWrapperRef.current || {}
const scrollRight = scrollWidth - (scrollLeft + clientWidth)
setShowRightArrow(scrollRight > 0)
}, [isExpanded])
return {
containerRef,
firstColumnWidth,
getDisplayedValue,
handleScroll,
handleSortClick,
isExpanded,
paramColumnWidth,
params,
showExpandButton: !isMobileDevice && paramsCount > DISPLAYED_PARAMS_COLUMNS,
showLeftArrow,
showRightArrow,
slideLeft,
slideRight,
tableWrapperRef,
toggleIsExpanded,
}
}

@ -0,0 +1,166 @@
import { Fragment } from 'react'
import map from 'lodash/map'
import includes from 'lodash/includes'
import { PlayerParam } from 'requests'
import { T9n } from 'features/T9n'
import type { PlayersTableProps } from './types'
import { usePlayersTable } from './hooks'
import {
Container,
TableWrapper,
Table,
FirstColumn,
Cell,
Row,
PlayerNum,
PlayerNameWrapper,
PlayerName,
ParamShortTitle,
ArrowButtonRight,
ArrowButtonLeft,
Arrow,
ExpandButton,
Tooltip,
} from './styled'
export const PlayersTable = (props: PlayersTableProps) => {
const {
containerRef,
firstColumnWidth,
getDisplayedValue,
getFullName,
getPlayerParams,
handleScroll,
handleSortClick,
isExpanded,
paramColumnWidth,
params,
players,
showExpandButton,
showLeftArrow,
showRightArrow,
slideLeft,
slideRight,
sortCondition,
tableWrapperRef,
toggleIsExpanded,
} = usePlayersTable(props)
return (
<Container
ref={containerRef}
isExpanded={isExpanded}
>
<TableWrapper
ref={tableWrapperRef}
isExpanded={isExpanded}
onScroll={handleScroll}
>
{!isExpanded && (
<Fragment>
{showLeftArrow && (
<ArrowButtonLeft
aria-label='Scroll to left'
onClick={slideLeft}
>
<Arrow direction='left' />
</ArrowButtonLeft>
)}
{showRightArrow && (
<ArrowButtonRight
aria-label='Scroll to right'
onClick={slideRight}
>
<Arrow direction='right' />
</ArrowButtonRight>
)}
</Fragment>
)}
<FirstColumn columnWidth={firstColumnWidth}>
<Row>
<Cell>
{showExpandButton && (
<ExpandButton
aria-label={isExpanded ? 'Reduce' : 'Expand'}
onClick={toggleIsExpanded}
>
<Arrow direction={isExpanded ? 'right' : 'left'} />
<Arrow direction={isExpanded ? 'right' : 'left'} />
</ExpandButton>
)}
</Cell>
</Row>
{map(players, (player) => {
const fullName = getFullName(player)
return (
<Row key={player.id}>
<Cell>
<PlayerNum>
{player.club_shirt_num}
</PlayerNum>{' '}
<PlayerNameWrapper>
<PlayerName columnWidth={firstColumnWidth}>
{fullName}
</PlayerName>
<Tooltip>
<PlayerName>{fullName}</PlayerName>
</Tooltip>
</PlayerNameWrapper>
</Cell>
</Row>
)
})}
</FirstColumn>
<Table>
<Row>
{map(params, ({
id,
lexic,
lexica_short,
}) => (
<Cell
key={id}
columnWidth={paramColumnWidth}
onClick={handleSortClick(id)}
sorted={sortCondition.paramId === id}
headerCell
>
<ParamShortTitle t={lexica_short || ''} />
<Tooltip>
<T9n t={lexic} />
</Tooltip>
</Cell>
))}
</Row>
{map(players, (player) => (
<Row key={player.id}>
{map(params, ({ id }) => {
const playerParam = getPlayerParams(player.id)[id] as PlayerParam | undefined
const value = playerParam ? getDisplayedValue(playerParam) : '-'
const clickable = Boolean(playerParam?.clickable) && !includes([0, '-'], value)
const sorted = sortCondition.paramId === id
return (
<Cell
columnWidth={paramColumnWidth}
key={id}
clickable={clickable}
sorted={sorted}
>
{value}
</Cell>
)
})}
</Row>
))}
</Table>
</TableWrapper>
</Container>
)
}

@ -0,0 +1,241 @@
import styled, { css } from 'styled-components/macro'
import { isMobileDevice } from 'config'
import { customScrollbar } from 'features/Common'
import { TooltipWrapper } from 'features/Tooltip'
import {
ArrowButton as ArrowButtonBase,
Arrow as ArrowBase,
} from 'features/HeaderFilters/components/DateFilter/styled'
import { T9n } from 'features/T9n'
type ContainerProps = {
isExpanded?: boolean,
}
export const Container = styled.div<ContainerProps>`
${({ isExpanded }) => (isExpanded
? ''
: css`
position: relative;
`)}
`
type TableWrapperProps = {
isExpanded?: boolean,
}
export const TableWrapper = styled.div<TableWrapperProps>`
display: flex;
max-width: 100%;
max-height: calc(100vh - 235px);
border-radius: 5px;
overflow-x: auto;
scroll-behavior: smooth;
background-color: #333333;
z-index: 50;
${customScrollbar}
${({ isExpanded }) => (isExpanded
? css`
position: absolute;
right: 14px;
`
: '')}
`
export const Table = styled.div`
flex-grow: 1;
border-radius: 5px;
border-collapse: collapse;
letter-spacing: -0.078px;
`
export const Tooltip = styled(TooltipWrapper)`
left: auto;
padding: 2px 10px;
border-radius: 6px;
transform: none;
font-size: 11px;
line-height: 1;
color: ${({ theme }) => theme.colors.black};
::before {
display: none;
}
`
export const ParamShortTitle = styled(T9n)`
text-transform: uppercase;
`
export const Row = styled.div`
display: flex;
width: 100%;
height: 45px;
border-bottom: 0.5px solid rgba(255, 255, 255, 0.5);
:first-child {
position: sticky;
left: 0;
top: 0;
z-index: 1;
}
`
type TdProps = {
clickable?: boolean,
columnWidth?: number,
headerCell?: boolean,
sorted?: boolean,
}
export const Cell = styled.div.attrs(({ clickable }: TdProps) => ({
...clickable && { tabIndex: 0 },
}))<TdProps>`
position: relative;
display: flex;
justify-content: center;
align-items: center;
flex-shrink: 0;
width: ${({ columnWidth }) => (columnWidth ? `${columnWidth}px` : 'auto')};
font-size: 11px;
color: ${({
clickable,
headerCell,
theme,
}) => (clickable && !headerCell ? '#5EB2FF' : theme.colors.white)};
white-space: nowrap;
background-color: #333333;
${Tooltip} {
top: 35px;
}
:hover {
${Tooltip} {
display: block;
}
}
${({ headerCell }) => (headerCell
? ''
: css`
:first-child {
justify-content: unset;
padding-left: 13px;
color: ${({ theme }) => theme.colors.white};
}
`)}
${({ sorted }) => (sorted
? css`
font-weight: bold;
`
: '')}
${({ clickable, headerCell }) => (clickable || headerCell
? css`
cursor: pointer;
`
: '')}
`
type FirstColumnProps = {
columnWidth?: number,
}
export const FirstColumn = styled.div<FirstColumnProps>`
position: sticky;
left: 0;
width: ${({ columnWidth }) => (columnWidth ? `${columnWidth}px` : 'auto')};
`
export const PlayerNum = styled.span`
display: inline-block;
width: 20px;
flex-shrink: 0;
text-align: center;
color: rgba(255, 255, 255, 0.5);
`
type PlayerNameProps = {
columnWidth?: number,
}
export const PlayerName = styled.span<PlayerNameProps>`
display: inline-block;
margin-top: 2px;
text-overflow: ellipsis;
overflow: hidden;
${({ columnWidth }) => (columnWidth
? css`
max-width: calc(${columnWidth}px - 31px);
`
: css`
max-width: 110px;
`)}
`
export const PlayerNameWrapper = styled.span`
position: relative;
${Tooltip} {
top: 15px;
}
:hover {
${Tooltip} {
display: block;
}
}
`
const ArrowButton = styled(ArrowButtonBase)`
position: absolute;
width: 17px;
margin-top: 2px;
background-color: #333333;
z-index: 3;
${isMobileDevice
? css`
height: 45px;
margin-top: 0;
`
: ''};
`
export const ArrowButtonRight = styled(ArrowButton)`
right: 0;
`
export const ArrowButtonLeft = styled(ArrowButton)`
left: 75px;
`
export const Arrow = styled(ArrowBase)`
width: 10px;
height: 10px;
${isMobileDevice
? css`
border-color: ${({ theme }) => theme.colors.white};
`
: ''};
`
export const ExpandButton = styled(ArrowButton)`
left: 20px;
top: 0;
${Arrow} {
left: 0;
:last-child {
margin-left: 7px;
}
}
`

@ -0,0 +1,8 @@
export type PlayersTableProps = {
teamId: number,
}
export type SortCondition = {
dir: 'asc' | 'desc',
paramId: number | null,
}

@ -0,0 +1,31 @@
import isEmpty from 'lodash/isEmpty'
import type { Playlists, PlaylistOption } from 'features/MatchPage/types'
import type { MatchInfo } from 'requests'
import { PlayersPlaylists } from '../PlayersPlaylists'
type Props = {
onSelect: (option: PlaylistOption) => void,
playlists: Playlists,
profile: MatchInfo,
selectedPlaylist?: PlaylistOption,
}
export const TabPlayers = ({
onSelect,
playlists,
profile,
selectedPlaylist,
}: Props) => {
if (isEmpty(playlists.players.team1)) return null
return (
<PlayersPlaylists
profile={profile}
players={playlists.players}
selectedMathPlaylist={selectedPlaylist}
onSelect={onSelect}
/>
)
}

@ -0,0 +1,10 @@
export enum Tabs {
TEAMS,
TEAM1,
TEAM2,
}
export enum StatsType {
FINAL_STATS,
CURRENT_STATS,
}

@ -0,0 +1,68 @@
import { useEffect, useState } from 'react'
import isEmpty from 'lodash/isEmpty'
import { useMatchPageStore } from 'features/MatchPage/store'
import { StatsType, Tabs } from './config'
export const useTabStats = () => {
const [selectedTab, setSelectedTab] = useState<Tabs>(Tabs.TEAMS)
const {
isEmptyPlayersStats,
profile: matchProfile,
setStatsType,
statsType,
teamsStats,
} = useMatchPageStore()
const isFinalStatsType = statsType === StatsType.FINAL_STATS
const switchTitleLexic = isFinalStatsType ? 'final_stats' : 'current_stats'
const tooltipLexic = isFinalStatsType ? 'display_all_stats' : 'display_stats_according_to_video'
const isVisibleTeamsTab = !isEmpty(teamsStats)
const isVisibleTeam1PlayersTab = Boolean(
matchProfile && !isEmptyPlayersStats(matchProfile.team1.id),
)
const isVisibleTeam2PlayersTab = Boolean(
matchProfile && !isEmptyPlayersStats(matchProfile.team2.id),
)
const toggleStatsType = () => {
const newStatsType = isFinalStatsType ? StatsType.CURRENT_STATS : StatsType.FINAL_STATS
setStatsType(newStatsType)
}
useEffect(() => {
switch (true) {
case isVisibleTeamsTab:
setSelectedTab(Tabs.TEAMS)
break
case isVisibleTeam1PlayersTab:
setSelectedTab(Tabs.TEAM1)
break
case isVisibleTeam2PlayersTab:
setSelectedTab(Tabs.TEAM2)
break
default:
}
}, [isVisibleTeam1PlayersTab, isVisibleTeam2PlayersTab, isVisibleTeamsTab])
return {
isFinalStatsType,
isVisibleTeam1PlayersTab,
isVisibleTeam2PlayersTab,
isVisibleTeamsTab,
selectedTab,
setSelectedTab,
switchTitleLexic,
toggleStatsType,
tooltipLexic,
}
}

@ -0,0 +1,103 @@
import { isMobileDevice } from 'config/userAgent'
import { getTeamAbbr } from 'helpers'
import { Tooltip } from 'features/Tooltip'
import { T9n } from 'features/T9n'
import { useMatchPageStore } from 'features/MatchPage/store'
import { Name } from 'features/Name'
import { Tabs } from './config'
import { useTabStats } from './hooks'
import { PlayersTable } from '../PlayersTable'
import { TeamsStatsTable } from '../TeamsStatsTable'
import {
Container,
Header,
TabList,
Tab,
Switch,
SwitchTitle,
SwitchButton,
} from './styled'
const tabPanes = {
[Tabs.TEAMS]: TeamsStatsTable,
[Tabs.TEAM1]: PlayersTable,
[Tabs.TEAM2]: PlayersTable,
}
export const TabStats = () => {
const {
isFinalStatsType,
isVisibleTeam1PlayersTab,
isVisibleTeam2PlayersTab,
isVisibleTeamsTab,
selectedTab,
setSelectedTab,
switchTitleLexic,
toggleStatsType,
tooltipLexic,
} = useTabStats()
const { profile: matchProfile } = useMatchPageStore()
const TabPane = tabPanes[selectedTab]
if (!matchProfile) return null
const { team1, team2 } = matchProfile
return (
<Container>
<Header>
<TabList>
{isVisibleTeamsTab && (
<Tab
aria-pressed={selectedTab === Tabs.TEAMS}
onClick={() => setSelectedTab(Tabs.TEAMS)}
>
<T9n t='team' />
</Tab>
)}
{isVisibleTeam1PlayersTab && (
<Tab
aria-pressed={selectedTab === Tabs.TEAM1}
onClick={() => setSelectedTab(Tabs.TEAM1)}
>
<Name nameObj={{
name_eng: team1.abbrev_eng || getTeamAbbr(team1.name_eng),
name_rus: team1.abbrev_rus || getTeamAbbr(team1.name_rus),
}}
/>
</Tab>
)}
{isVisibleTeam2PlayersTab && (
<Tab
aria-pressed={selectedTab === Tabs.TEAM2}
onClick={() => setSelectedTab(Tabs.TEAM2)}
>
<Name nameObj={{
name_eng: team2.abbrev_eng || getTeamAbbr(team2.name_eng),
name_rus: team2.abbrev_rus || getTeamAbbr(team2.name_rus),
}}
/>
</Tab>
)}
</TabList>
<Switch>
<SwitchTitle t={switchTitleLexic} />
<SwitchButton
isFinalStatsType={isFinalStatsType}
onClick={toggleStatsType}
>
{!isMobileDevice && <Tooltip lexic={tooltipLexic} />}
</SwitchButton>
</Switch>
</Header>
<TabPane
teamId={selectedTab === Tabs.TEAM1 ? team1.id : team2.id}
/>
</Container>
)
}

@ -0,0 +1,110 @@
import styled, { css } from 'styled-components/macro'
import { TooltipWrapper } from 'features/Tooltip'
import { T9n } from 'features/T9n'
export const Container = styled.div``
export const Header = styled.div`
display: flex;
justify-content: space-between;
margin-bottom: 23px;
`
export const TabList = styled.div.attrs({ role: 'tablist' })`
display: flex;
`
export const Tab = styled.button.attrs({ role: 'tab' })`
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 10px 10px;
font-size: 12px;
color: ${({ theme }) => theme.colors.white};
opacity: 0.4;
cursor: pointer;
border: none;
background: none;
border-bottom: 2px solid transparent;
&[aria-pressed="true"] {
opacity: 1;
border-color: currentColor;
}
`
export const Switch = styled.div`
display: flex;
`
export const SwitchTitle = styled(T9n)`
font-size: 12px;
color: ${({ theme }) => theme.colors.white};
white-space: nowrap;
`
type SwitchButtonProps = {
isFinalStatsType: boolean,
}
export const SwitchButton = styled.button<SwitchButtonProps>`
position: relative;
width: 20px;
height: 7px;
margin-left: 5px;
margin-top: 5px;
border-radius: 2px;
border: none;
border: 1px solid ${({ theme }) => theme.colors.white};
cursor: pointer;
${TooltipWrapper} {
left: auto;
right: 0;
top: 15px;
padding: 2px 10px;
border-radius: 6px;
transform: none;
font-size: 11px;
line-height: 1;
::before {
display: none;
}
}
:hover {
${TooltipWrapper} {
display: block;
}
}
${({ isFinalStatsType, theme }) => (!isFinalStatsType
? css`
background-image: linear-gradient(
to right,
${theme.colors.white} 33.333%,
${theme.colors.black} 33.333%,
${theme.colors.black} 66.666%,
${theme.colors.white} 66.666%,
${theme.colors.white} 72%,
${theme.colors.black} 72%,
${theme.colors.black} 100%)
`
: css`
border-color: transparent;
background-image: linear-gradient(
to right,
${theme.colors.white} 33.333%,
${theme.colors.black} 33.333%,
${theme.colors.black} 38%,
${theme.colors.white} 38%,
${theme.colors.white} 66.666%,
${theme.colors.black} 66.666%,
${theme.colors.black} 72%,
${theme.colors.white} 72%,
${theme.colors.white} 100%)
`
)}
`

@ -1,36 +1,64 @@
import { Fragment } from 'react'
import {
Fragment,
useMemo,
useRef,
} from 'react'
import isEmpty from 'lodash/isEmpty'
import size from 'lodash/size'
import filter from 'lodash/filter'
import type { PlaylistOption, Playlists } from 'features/MatchPage/types'
import type {
PlaylistOption,
Playlists,
TournamentData,
} from 'features/MatchPage/types'
import type { MatchInfo } from 'requests'
import { DropdownSection } from '../DropdownSection'
import { MatchPlaylists } from '../MatchPlaylists'
import { MatchPlaylists, LIST_ITEM_INDENT } from '../MatchPlaylists'
import { SideInterviews } from '../SideInterviews'
import { PlayersPlaylists } from '../PlayersPlaylists'
import { Matches } from '../Matches'
type Props = {
onSelect: (option: PlaylistOption) => void,
playListFilter: number,
playlists: Playlists,
profile: MatchInfo,
selectedPlaylist?: PlaylistOption,
tournamentData: TournamentData,
}
export const TabWatch = ({
onSelect,
playListFilter,
playlists,
profile,
selectedPlaylist,
}: Props) => (
tournamentData,
}: Props) => {
const matchPlaylistsRef = useRef<HTMLUListElement>(null)
const additionalScrollHeight = (matchPlaylistsRef.current?.clientHeight || 0) + LIST_ITEM_INDENT
const filteredPlayListByDuration = useMemo(() => (
filter(playlists.match, (playlist) => (
profile?.live
? Boolean(playlist.duration) || (playlist.id === 'full_game')
: Boolean(playlist.duration)
))
), [playlists.match, profile?.live])
return (
<Fragment>
{playListFilter > 1 && (
<MatchPlaylists
playlists={playlists.match}
ref={matchPlaylistsRef}
playlists={filteredPlayListByDuration}
selectedMathPlaylist={selectedPlaylist}
onSelect={onSelect}
live={profile?.live}
/>
)}
<DropdownSection
itemsCount={size(playlists.interview)}
title={playlists.lexics?.interview}
@ -41,13 +69,11 @@ export const TabWatch = ({
onSelect={onSelect}
/>
</DropdownSection>
{!isEmpty(playlists.players.team1) && (
<PlayersPlaylists
<Matches
profile={profile}
players={playlists.players}
selectedMathPlaylist={selectedPlaylist}
onSelect={onSelect}
tournamentData={tournamentData}
additionalScrollHeight={additionalScrollHeight}
/>
)}
</Fragment>
)
)
}

@ -0,0 +1,31 @@
import isNumber from 'lodash/isNumber'
import find from 'lodash/find'
import round from 'lodash/round'
import type { Param } from 'requests'
import { useMatchPageStore } from 'features/MatchPage/store'
export const useTeamsStatsTable = () => {
const { profile, teamsStats } = useMatchPageStore()
const getDisplayedValue = (val: any) => (
isNumber(val) ? round(val, 2) : '-'
)
const getStatItemById = (paramId: number) => {
if (!profile) return null
return find(teamsStats[profile?.team2.id], ({ param1 }) => param1.id === paramId) || null
}
const isClickable = (param: Param) => (
Boolean(param.val) && param.clickable
)
return {
getDisplayedValue,
getStatItemById,
isClickable,
}
}

@ -0,0 +1,93 @@
import { Fragment } from 'react'
import map from 'lodash/map'
import { useMatchPageStore } from 'features/MatchPage/store'
import { useLexicsStore } from 'features/LexicsStore'
import { useTeamsStatsTable } from './hooks'
import {
Container,
Row,
TeamShortName,
ParamValueContainer,
ParamValue,
StatItemTitle,
Divider,
} from './styled'
export const TeamsStatsTable = () => {
const { profile, teamsStats } = useMatchPageStore()
const {
getDisplayedValue,
getStatItemById,
isClickable,
} = useTeamsStatsTable()
const { lang } = useLexicsStore()
if (!profile) return null
return (
<Container>
<Row>
<TeamShortName
nameObj={profile.team1}
prefix='abbrev_'
/>
<TeamShortName
nameObj={profile.team2}
prefix='abbrev_'
/>
</Row>
{map(teamsStats[profile.team1.id], (team1StatItem) => {
const team2StatItem = getStatItemById(team1StatItem.param1.id)
const statItemTitle = team1StatItem[`name_${lang === 'ru' ? 'ru' : 'en'}`]
return (
<Row key={team1StatItem.param1.id}>
<ParamValueContainer>
<ParamValue
clickable={isClickable(team1StatItem.param1)}
>
{getDisplayedValue(team1StatItem.param1.val)}
</ParamValue>
{team1StatItem.param2 && (
<Fragment>
<Divider>/</Divider>
<ParamValue
clickable={isClickable(team1StatItem.param2)}
>
{getDisplayedValue(team1StatItem.param2.val)}
</ParamValue>
</Fragment>
)}
</ParamValueContainer>
<StatItemTitle>{statItemTitle}</StatItemTitle>
{team2StatItem && (
<ParamValueContainer>
<ParamValue
clickable={isClickable(team2StatItem.param1)}
>
{getDisplayedValue(team2StatItem.param1.val)}
</ParamValue>
{team2StatItem.param2 && (
<Fragment>
<Divider>/</Divider>
<ParamValue
clickable={isClickable(team2StatItem.param2)}
>
{getDisplayedValue(team2StatItem.param2.val)}
</ParamValue>
</Fragment>
)}
</ParamValueContainer>
)}
</Row>
)
})}
</Container>
)
}

@ -0,0 +1,65 @@
import styled, { css } from 'styled-components/macro'
import { Name } from 'features/Name'
export const Container = styled.div`
width: 100%;
font-size: 11px;
overflow: hidden;
border-radius: 5px;
background-color: #333333;
`
export const TeamShortName = styled(Name)`
color: ${({ theme }) => theme.colors.white};
letter-spacing: -0.078px;
text-transform: uppercase;
font-weight: 600;
opacity: 0.5;
`
export const Row = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
height: 45px;
padding: 0 12px;
border-bottom: 0.5px solid rgba(255, 255, 255, 0.5);
:last-child {
border-bottom: none;
}
`
export const ParamValueContainer = styled.div``
type TParamValue = {
clickable?: boolean,
}
export const ParamValue = styled.span.attrs(({ clickable }: TParamValue) => ({
...clickable && { tabIndex: 0 },
}))<TParamValue>`
font-weight: 600;
color: ${({ clickable, theme }) => (clickable ? '#5EB2FF' : theme.colors.white)};
${({ clickable }) => (clickable
? css`
cursor: pointer;
`
: '')}
`
export const StatItemTitle = styled.span`
color: ${({ theme }) => theme.colors.white};
letter-spacing: -0.078px;
text-transform: uppercase;
font-weight: 600;
opacity: 0.5;
`
export const Divider = styled.span`
color: ${({ theme }) => theme.colors.white};
opacity: 0.5;
font-weight: 600;
`

@ -1,5 +1,6 @@
export enum Tabs {
WATCH,
EVENTS,
VIDEO
STATS,
PLAYERS,
}

@ -5,6 +5,8 @@ import {
} from 'react'
import reduce from 'lodash/reduce'
import isEmpty from 'lodash/isEmpty'
import compact from 'lodash/compact'
import { useMatchPageStore } from 'features/MatchPage/store'
@ -14,12 +16,15 @@ export const useMatchSidePlaylists = () => {
const {
closePopup,
events,
isEmptyPlayersStats,
matchPlaylists: playlists,
profile: matchProfile,
teamsStats,
tournamentData,
} = useMatchPageStore()
const [selectedTab, setSelectedTab] = useState<Tabs>(Tabs.WATCH)
const isWatchTabVisible = useMemo(() => {
const playListFilter = reduce(
const playListFilter = useMemo(() => reduce(
playlists.match,
(acc, item) => {
let result = acc
@ -27,17 +32,37 @@ export const useMatchSidePlaylists = () => {
return result
},
0,
)
return playListFilter > 1
}, [playlists])
), [playlists.match])
const isWatchTabVisible = useMemo(() => (
playListFilter > 1 || tournamentData.matchDates.length > 1
), [playListFilter, tournamentData.matchDates.length])
const isEventTabVisible = useMemo(() => (
events.length > 0
), [events])
const isVideoTabVisible = useMemo(() => (
tournamentData.matches.length > 1
), [tournamentData])
const isPlayersTabVisible = useMemo(() => (
!isEmpty(playlists.players.team1)
), [playlists.players.team1])
const isStatsTabVisible = useMemo(() => (
!isEmpty(teamsStats)
|| (matchProfile?.team1.id && !isEmptyPlayersStats(matchProfile.team1.id))
|| (matchProfile?.team2.id && !isEmptyPlayersStats(matchProfile.team2.id))
), [
isEmptyPlayersStats,
matchProfile?.team1.id,
matchProfile?.team2.id,
teamsStats,
])
const hasLessThanFourTabs = compact([
isWatchTabVisible,
isEventTabVisible,
isPlayersTabVisible,
// isStatsTabVisible,
]).length < 4
useEffect(() => {
switch (true) {
@ -47,21 +72,32 @@ export const useMatchSidePlaylists = () => {
case isEventTabVisible:
setSelectedTab(Tabs.EVENTS)
break
case isVideoTabVisible:
setSelectedTab(Tabs.VIDEO)
case isPlayersTabVisible:
setSelectedTab(Tabs.PLAYERS)
break
// case isStatsTabVisible:
// setSelectedTab(Tabs.STATS)
// break
}
}, [isEventTabVisible, isVideoTabVisible, isWatchTabVisible])
}, [
isEventTabVisible,
isPlayersTabVisible,
// isStatsTabVisible,
isWatchTabVisible,
])
useEffect(() => {
if (selectedTab !== Tabs.EVENTS) closePopup()
}, [selectedTab, closePopup])
return {
hasLessThanFourTabs,
isEventTabVisible,
isVideoTabVisible,
isPlayersTabVisible,
isStatsTabVisible,
isWatchTabVisible,
onTabClick: setSelectedTab,
playListFilter,
selectedTab,
}
}

@ -6,8 +6,6 @@ import {
import type { TCircleAnimation, TSetCircleAnimation } from 'features/CircleAnimationBar'
import type { PlaylistOption } from 'features/MatchPage/types'
import { Tab, TabsGroup } from 'features/Common'
import { T9n } from 'features/T9n'
import { useMatchPageStore } from 'features/MatchPage/store'
import { useEventListener } from 'hooks'
@ -17,18 +15,24 @@ import { isIOS } from 'config/userAgent'
import { Tabs } from './config'
import { TabEvents } from './components/TabEvents'
import { TabWatch } from './components/TabWatch'
import { TabVideo } from './components/TabVideo'
import { TabPlayers } from './components/TabPlayers'
import { TabStats } from './components/TabStats'
import { useMatchSidePlaylists } from './hooks'
import {
Wrapper,
TabsWrapper,
TabsGroup,
Tab,
TabIcon,
TabTitle,
Container,
} from './styled'
const tabPanes = {
[Tabs.WATCH]: TabWatch,
[Tabs.EVENTS]: TabEvents,
[Tabs.VIDEO]: TabVideo,
[Tabs.STATS]: TabStats,
[Tabs.PLAYERS]: TabPlayers,
}
type Props = {
@ -53,10 +57,13 @@ export const MatchSidePlaylists = ({
} = useMatchPageStore()
const {
hasLessThanFourTabs,
isEventTabVisible,
isVideoTabVisible,
isPlayersTabVisible,
// isStatsTabVisible,
isWatchTabVisible,
onTabClick,
playListFilter,
selectedTab,
} = useMatchSidePlaylists()
@ -99,38 +106,50 @@ export const MatchSidePlaylists = ({
return (
<Wrapper ref={containerRef}>
<TabsWrapper>
<TabsGroup>
<TabsGroup hasLessThanFourTabs={hasLessThanFourTabs}>
{isWatchTabVisible ? (
<Tab
selected={selectedTab === Tabs.WATCH}
aria-pressed={selectedTab === Tabs.WATCH}
onClick={() => onTabClick(Tabs.WATCH)}
>
<T9n t='watch' />
<TabIcon icon='watch' />
<TabTitle t='watch' />
</Tab>
) : null}
{isEventTabVisible ? (
<Tab
selected={selectedTab === Tabs.EVENTS}
aria-pressed={selectedTab === Tabs.EVENTS}
onClick={() => onTabClick(Tabs.EVENTS)}
>
<T9n t='actions' />
<TabIcon icon='plays' />
<TabTitle t='actions' />
</Tab>
) : null}
{isVideoTabVisible ? (
{isPlayersTabVisible ? (
<Tab
selected={selectedTab === Tabs.VIDEO}
onClick={() => onTabClick(Tabs.VIDEO)}
aria-pressed={selectedTab === Tabs.PLAYERS}
onClick={() => onTabClick(Tabs.PLAYERS)}
>
<T9n t='video' />
<TabIcon icon='players' />
<TabTitle t='players' />
</Tab>
) : null}
{/* {isStatsTabVisible ? (
<Tab
aria-pressed={selectedTab === Tabs.STATS}
onClick={() => onTabClick(Tabs.STATS)}
>
<TabIcon icon='stats' />
<TabTitle t='stats' />
</Tab>
) : null} */}
</TabsGroup>
</TabsWrapper>
<Container
hasScroll={hasTabPaneScroll}
ref={tabPaneContainerRef}
forVideoTab={selectedTab === Tabs.VIDEO}
forWatchTab={selectedTab === Tabs.WATCH}
>
<TabPane
setCircleAnimation={setCircleAnimation}
@ -140,6 +159,7 @@ export const MatchSidePlaylists = ({
playlists={playlists}
profile={profile}
selectedPlaylist={selectedPlaylist}
playListFilter={playListFilter}
/>
</Container>
</Wrapper>

@ -4,6 +4,7 @@ import { devices } from 'config/devices'
import { isMobileDevice } from 'config/userAgent'
import { customScrollbar } from 'features/Common'
import { T9n } from 'features/T9n'
export const Wrapper = styled.div`
padding-right: 14px;
@ -19,7 +20,7 @@ export const Wrapper = styled.div`
`
export const TabsWrapper = styled.div`
padding-left: 14px;
padding: 0 30px;
${isMobileDevice
? css`
@ -28,8 +29,81 @@ export const TabsWrapper = styled.div`
: ''};
`
type TabsGroupProps = {
hasLessThanFourTabs?: boolean,
}
export const TabsGroup = styled.div.attrs({ role: 'tablist' })<TabsGroupProps>`
display: flex;
height: 45px;
padding-top: 10px;
${({ hasLessThanFourTabs }) => (hasLessThanFourTabs
? css`
height: 40px;
${Tab} {
justify-content: center;
flex-direction: row;
gap: 5px;
}
`
: '')}
${isMobileDevice
? css`
`
: ''};
`
export const TabTitle = styled(T9n)`
font-size: 10px;
font-weight: 500;
text-transform: uppercase;
color: ${({ theme }) => theme.colors.white};
`
export const Tab = styled.button.attrs({ role: 'tab' })`
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
flex: 1;
opacity: 0.4;
cursor: pointer;
border: none;
background: none;
&[aria-pressed="true"], :hover {
opacity: 1;
${TabTitle} {
font-weight: 600;
}
}
`
type TabIconProps = {
icon: 'watch' | 'plays' | 'players' | 'stats',
}
export const TabIcon = styled.div<TabIconProps>`
width: 22px;
height: 22px;
background-image: url(/images/matchTabs/${({ icon }) => `${icon}.svg`});
background-repeat: no-repeat;
background-position: center;
background-size: contain;
${({ icon }) => (icon === 'players'
? css`
background-size: 23px;
`
: '')}
`
type TContainer = {
forVideoTab?: boolean,
forWatchTab?: boolean,
hasScroll: boolean,
}
@ -37,14 +111,13 @@ export const Container = styled.div<TContainer>`
width: 320px;
margin-top: 14px;
max-height: calc(100vh - 130px);
overflow-y: ${({ forVideoTab }) => (forVideoTab ? 'hidden' : 'auto')};
padding-right: ${({ forVideoTab }) => (forVideoTab ? '0' : '')};
overflow-y: ${({ forWatchTab }) => (forWatchTab ? 'hidden' : 'auto')};
padding-right: ${({ forWatchTab }) => (forWatchTab ? '0' : '')};
padding-left: 14px;
padding-right: ${({ hasScroll }) => (hasScroll ? '10px' : '')};
${customScrollbar}
@media ${devices.tablet} {
margin-top: 15px;
}

@ -14,9 +14,18 @@ import { useVolume } from 'features/VideoPlayer/hooks/useVolume'
import { useNoNetworkPopupStore } from 'features/NoNetworkPopup'
import { useMatchPageStore } from 'features/MatchPage/store'
import { useEventListener, useObjectState } from 'hooks'
import {
useEventListener,
useInterval,
useObjectState,
usePageParams,
} from 'hooks'
import { MatchInfo } from 'requests'
import {
MatchInfo,
saveMatchStats,
VIEW_INTERVAL_MS,
} from 'requests'
import { useProgressChangeHandler } from './useProgressChangeHandler'
import { usePlayingHandlers } from './usePlayingHandlers'
@ -47,6 +56,7 @@ export type Props = {
chapters: Chapters,
isOpenPopup?: boolean,
onError?: () => void,
onPlayerProgressChange?: (ms: number) => void,
onPlayingChange: (playing: boolean) => void,
profile: MatchInfo,
setCircleAnimation: TSetCircleAnimation,
@ -55,6 +65,7 @@ export type Props = {
export const useMultiSourcePlayer = ({
chapters,
onError,
onPlayerProgressChange,
onPlayingChange,
setCircleAnimation,
}: Props) => {
@ -63,6 +74,11 @@ export const useMultiSourcePlayer = ({
playNextEpisode,
} = useMatchPageStore()
const { profileId, sportType } = usePageParams()
/** время для сохранения статистики просмотра матча */
const timeForStatistics = useRef(0)
const numberOfChapters = size(chapters)
const [
{
@ -184,7 +200,10 @@ export const useMultiSourcePlayer = ({
const chapter = getActiveChapter()
const value = Math.max(playedMs - chapter.startOffsetMs, 0)
timeForStatistics.current = (value + chapter.startMs) / 1000
setPlayerState({ playedProgress: value })
onPlayerProgressChange?.(playedMs + chapter.startMs)
}
const onEnded = () => {
@ -267,6 +286,27 @@ export const useMultiSourcePlayer = ({
}
}, [ready, videoRef])
// ведем статистику просмотра матча
const { start: startCollectingStats, stop: stopCollectingStats } = useInterval({
callback: () => {
saveMatchStats({
matchId: profileId,
matchSecond: timeForStatistics.current,
sportType,
})
},
intervalDuration: VIEW_INTERVAL_MS,
startImmediate: false,
})
useEffect(() => {
if (playing) {
startCollectingStats()
} else {
stopCollectingStats()
}
}, [playing, startCollectingStats, stopCollectingStats])
return {
activeChapterIndex,
activePlayer,

@ -114,7 +114,7 @@ export const ControlsWeb = (controlsProps: { props: ControlsPropsExtended }) =>
)}
<ChromeCast src={src} />
{document.pictureInPictureEnabled && (
<PiP videoRef={videoRef} />
<PiP videoRef={videoRef} isPlaying={playing} />
)}
<Settings
onSelect={onQualitySelect}

@ -7,7 +7,7 @@ import { PlayerWrapper } from '../../styled'
import { useVideoPlayer, Props } from '../../hooks'
export const YoutubePlayer = (props: Props) => {
const { isOpenPopup, profile } = useMatchPageStore()
const { isOpenFiltersPopup, profile } = useMatchPageStore()
const {
onMouseMove,
@ -34,7 +34,7 @@ export const YoutubePlayer = (props: Props) => {
onTouchStart={onTouchStart}
onTouchEnd={onTouchEnd}
>
{isOpenPopup && <FiltersPopup />}
{isOpenFiltersPopup && <FiltersPopup />}
<YouTube
videoId={key}
opts={{

@ -1,5 +1,6 @@
import type { MouseEvent } from 'react'
import {
useRef,
useCallback,
useEffect,
useState,
@ -11,8 +12,12 @@ import isEmpty from 'lodash/isEmpty'
import { isIOS } from 'config/userAgent'
import { useObjectState } from 'hooks/useObjectState'
import { useEventListener } from 'hooks/useEventListener'
import {
useObjectState,
useEventListener,
usePageParams,
useInterval,
} from 'hooks'
import type { TSetCircleAnimation } from 'features/CircleAnimationBar'
import type { Chapters } from 'features/StreamPlayer/types'
@ -21,6 +26,8 @@ import { useNoNetworkPopupStore } from 'features/NoNetworkPopup'
import { useLiveMatch } from 'features/MatchPage/components/LiveMatch/hooks'
import { useLexicsStore } from 'features/LexicsStore'
import { VIEW_INTERVAL_MS, saveMatchStats } from 'requests'
import { REWIND_SECONDS } from '../config'
import { useHlsPlayer } from './useHlsPlayer'
import { useFullscreen } from './useFullscreen'
@ -88,6 +95,10 @@ export const useVideoPlayer = ({
selectedPlaylist,
} = useLiveMatch()
const { lang } = useLexicsStore()
const { profileId, sportType } = usePageParams()
/** время для сохранения статистики просмотра матча */
const timeForStatistics = useRef(0)
const { url } = chapters[0] ?? { url: '' }
const numberOfChapters = size(chapters)
@ -208,6 +219,7 @@ export const useVideoPlayer = ({
const chapter = getActiveChapter()
const value = Math.max(playedMs - chapter.startOffsetMs, 0)
setPlayerState({ playedProgress: value })
timeForStatistics.current = (value + chapter.startMs) / 1000
progressChangeCallback(value / 1000)
}
@ -427,6 +439,27 @@ export const useVideoPlayer = ({
? 'La transmisión en vivo no está disponible temporalmente en dispositivos iOS'
: 'Live streaming is temporarily unavailable on iOS devices'
// ведем статистику просмотра матча
const { start: startCollectingStats, stop: stopCollectingStats } = useInterval({
callback: () => {
saveMatchStats({
matchId: profileId,
matchSecond: timeForStatistics.current,
sportType,
})
},
intervalDuration: VIEW_INTERVAL_MS,
startImmediate: false,
})
useEffect(() => {
if (playing) {
startCollectingStats()
} else {
stopCollectingStats()
}
}, [playing, startCollectingStats, stopCollectingStats])
return {
activeChapterIndex,
allPlayedProgress: playedProgress + getActiveChapter().startMs,

@ -30,7 +30,7 @@ import RewindMobile from './components/RewindMobile'
* HLS плеер, применяется на лайв и завершенных матчах
*/
export const StreamPlayer = (props: Props) => {
const { isOpenPopup, profile } = useMatchPageStore()
const { isOpenFiltersPopup, profile } = useMatchPageStore()
const { user } = useAuthStore()
const {
@ -96,7 +96,7 @@ export const StreamPlayer = (props: Props) => {
onTouchStart={onTouchStart}
onTouchEnd={onTouchEnd}
>
{isOpenPopup && <FiltersPopup />}
{isOpenFiltersPopup && <FiltersPopup />}
<LoaderWrapper buffering={buffering}>
<Loader color='#515151' />
</LoaderWrapper>

@ -25,7 +25,7 @@ export const TooltipWrapper = styled(TooltipBlockWrapper)`
`
type Props = {
children: ReactNode,
children?: ReactNode,
lexic: string,
}

@ -77,7 +77,7 @@ const UserAccount = () => {
{!isLffClient && !isInSportsClient && (
<StyledLink
target='_blank'
to={{ pathname: PAGES.about_the_project }}
to={{ pathname: client.about_the_project ?? PAGES.about_the_project }}
>
<T9n t='about_the_project' />
</StyledLink>

@ -0,0 +1,25 @@
import toUpper from 'lodash/toUpper'
import split from 'lodash/split'
import size from 'lodash/size'
import pipe from 'lodash/fp/pipe'
import take from 'lodash/fp/take'
import join from 'lodash/fp/join'
import map from 'lodash/fp/map'
export const getTeamAbbr = (teamName: string) => {
const nameParts = split(teamName, ' ')
return size(nameParts) > 1
? pipe(
map(take(1)),
join(''),
toUpper,
)(nameParts)
: pipe(
take(3),
join(''),
toUpper,
)(nameParts[0])
}

@ -8,3 +8,4 @@ export * from './secondsToHms'
export * from './redirectToUrl'
export * from './getRandomString'
export * from './selectedApi'
export * from './getTeamAbbr'

@ -4,3 +4,4 @@ export * from './useStorage'
export * from './useInterval'
export * from './useEventListener'
export * from './useObjectState'
export * from './usePageParams'

@ -22,6 +22,7 @@ export const usePageParams = () => {
return {
profileId: Number(pageId),
profileType: ProfileTypes[toUpper(profileName) as keyof typeof ProfileTypes],
sportName,
sportType: SportTypes[toUpper(sportName) as keyof typeof SportTypes],
}
}

@ -3,7 +3,7 @@
declare namespace NodeJS {
export interface ProcessEnv {
REACT_APP_CLIENT: 'instat' | 'facr' | 'lff' | 'insports',
REACT_APP_CLIENT: 'instat' | 'facr' | 'lff' | 'insports' | 'india' | 'tunis',
REACT_APP_ENV: 'production' | 'preproduction' | 'staging',
REACT_APP_STAGE: 'staging' | 'test-a' | 'test-b' | 'test-c' | 'test-d' | 'test-e' | 'test-f' | 'test-g' | 'test-h' | 'test-i' | 'test-j' | 'test',
REACT_APP_TYPE: 'auth-service' | 'ott',

@ -0,0 +1,65 @@
import isUndefined from 'lodash/isUndefined'
import { SportTypes } from 'config'
import { callApi } from 'helpers'
export type Player = {
birthday: string | null,
c_country: number,
c_gender: number,
club_f_team: number,
club_shirt_num: number,
firstname_eng: string,
firstname_national: string | null,
firstname_rus: string,
height: number | null,
id: number,
is_gk: boolean,
lastname_eng: string,
lastname_national: string | null,
lastname_rus: string,
national_f_team: number | null,
national_shirt_num: number,
nickname_eng: string | null,
nickname_rus: string | null,
weight: number | null,
}
type DataItem = {
players: Array<Player>,
team_id: number,
}
type Response = {
data?: Array<DataItem>,
error?: {
code: string,
message: string,
},
}
type GetMatchParticipantsArgs = {
matchId: number,
second?: number,
sportType: SportTypes,
}
export const getMatchParticipants = async ({
matchId,
second,
sportType,
}: GetMatchParticipantsArgs) => {
const config = {
method: 'GET',
}
const response: Response = await callApi({
config,
url: `http://136.243.17.103:8888/ask/participants?sport_id=${sportType}&match_id=${matchId}${isUndefined(second) ? '' : `&second=${second}`}`,
})
if (response.error) Promise.reject(response)
return Promise.resolve(response.data || [])
}

@ -0,0 +1,54 @@
import isUndefined from 'lodash/isUndefined'
import { callApi } from 'helpers'
export type PlayerParam = {
clickable: boolean,
data_type: string,
id: number,
lexic: number,
lexica_short: number | null,
markers: Array<number> | null,
name_en: string,
name_ru: string,
val: number | null,
}
export type PlayersStats = {
[playerId: string]: {
[paramId: string]: PlayerParam,
},
}
type Response = {
data?: PlayersStats,
error?: string,
message?: string,
}
type GetPlayersStatsArgs = {
matchId: number,
second?: number,
sportName: string,
teamId: number,
}
export const getPlayersStats = async ({
matchId,
second,
sportName,
teamId,
}: GetPlayersStatsArgs) => {
const config = {
method: 'GET',
}
const response: Response = await callApi({
config,
url: `http://136.243.17.103:8888/${sportName}/matches/${matchId}/teams/${teamId}/players/stats${isUndefined(second) ? '' : `?second=${second}`}`,
})
if (response.error) Promise.reject(response)
return Promise.resolve(response.data || {})
}

@ -0,0 +1,56 @@
import isUndefined from 'lodash/isUndefined'
import { callApi } from 'helpers'
export type Param = {
clickable: boolean,
data_type: string,
id: number,
lexic: number,
markers: Array<number>,
name_en: string,
name_ru: string,
val: number | null,
}
export type TeamStatItem = {
lexic: number,
name_en: string,
name_ru: string,
order: number,
param1: Param,
param2: Param | null,
}
type Response = {
data?: {
[teamId: string]: Array<TeamStatItem>,
},
error?: string,
message?: string,
}
type GetTeamsStatsArgs = {
matchId: number,
second?: number,
sportName: string,
}
export const getTeamsStats = async ({
matchId,
second,
sportName,
}: GetTeamsStatsArgs) => {
const config = {
method: 'GET',
}
const response: Response = await callApi({
config,
url: `http://136.243.17.103:8888/${sportName}/matches/${matchId}/teams/stats?group_num=0${isUndefined(second) ? '' : `&second=${second}`}`,
})
if (response.error) Promise.reject(response)
return Promise.resolve(response.data || {})
}

@ -18,6 +18,7 @@ export type UserInfo = {
},
email: string,
firstname: string | null,
has_subscription: boolean,
is_unsubscribed: boolean | null,
language: {
id: number | null,

@ -24,3 +24,7 @@ export * from './getMatchPlaylists'
export * from './getPlayerPlaylists'
export * from './getSubscriptions'
export * from './buySubscription'
export * from './saveMatchStats'
export * from './getTeamsStats'
export * from './getPlayersStats'
export * from './getMatchParticipants'

@ -0,0 +1,30 @@
import { SportTypes, VIEWS_API } from 'config'
import { callApi } from 'helpers'
type Props = {
matchId: number,
matchSecond: number,
sportType: SportTypes,
}
export const VIEW_INTERVAL_MS = 5000
export const saveMatchStats = ({
matchId,
matchSecond,
sportType,
}: Props) => {
const url = `${VIEWS_API}/user/view`
const config = {
body: {
interval: VIEW_INTERVAL_MS / 1000,
match_id: matchId,
second: matchSecond,
sport_id: sportType,
},
}
return callApi({ config, url })
}
Loading…
Cancel
Save