Compare commits

...

60 Commits

Author SHA1 Message Date
Rakov 8896df9f1f fix(refresh): refresh token fix 2 years ago
Rakov 10bca448dc fix(refresh): resfresh token fix 2 years ago
Rakov 7a2423568a fix(refresh): save refresh lff facr 2 years ago
Rakov ccb265cb2e Revert "fix(#699): auth token" 2 years ago
Rakov 3152372c39 fix(inposrt.live): insport.live makefile build 2 years ago
Rakov 4e43b9aea8 fix(insport.live): insport.live deploy 2 years ago
Rakov 0c5a1aac4f fix(#772): fqtv button lexic 2 years ago
Rakov fc3a3c052c fix(#699): auth token 2 years ago
Rakov 888ac35156 fix(favicon): favicon facr lff fqtv 2 years ago
Rakov 2b9b93ef31 fix(favicon): favicon lff 2 years ago
Rakov 1335d34b07 fix(preview): added preview width 2 years ago
Rakov 42623654bb fix(#720): timeline tile size 2 years ago
Rakov c0d06f8ef9 fix(#720): update penalty score 2 years ago
Rakov 2dba409aa7 fix(#720): fix timeline scroll 2 years ago
Rakov 8dce04b35d fix(#720): add timeline timezone 2 years ago
Rakov 642e3809dc fix(#720): timeline media query fix 2 years ago
Rakov c41acef4ce fix(#720): timeline mode 2 years ago
Rakov af912dca10 fix(toc): tOC link fix 2 years ago
Ruslan Khayrullin d06425df94 feat(in-645): likes 2 years ago
Rakov 0e0f24454e fix(#747): new broadcast endpoint 2 years ago
Rakov 2a95f32aeb fix(#727): save match stats fix 2 years ago
Rakov bef03ff120 fix(#ios): ios player fix 2 years ago
Rakov bd218b8888 fix(#750): check device,toc for india 2 years ago
Margarita 2d321ff6a2 feat(date-filter): date filter fixes 2 years ago
Margarita d4c4bd6e81 feat(in-738): user password fix 2 years ago
Rakov 0b2c38871a fix(#hot-fix): disabled watching for virtual user 2 years ago
andreidekterev 9f0c779246 fix(#734): add watermark for india 2 years ago
Farber Denis 02c72a87f1 fix(#in730): facr header settings popup temp disable 2 years ago
Margarita f70fe23926 feat(in-695): mobile ads fixes 2 years ago
Margarita 5e0b262839 feat(in-717): ads fixes 2 years ago
Ruslan Khayrullin 590ce1bb90 fix(in-723): stats fixes 2 years ago
Rakov 806a2519b6 fix(#677): auto authorize 2 years ago
Margarita efce351342 feat(in-710): personal info language fixes 2 years ago
Rakov f4b96c4668 fix(india): fix india PP nad TaC 2 years ago
Rakov 77f71b852b fix(#phonepe): fix iframe payment for mobile 2 years ago
Ruslan Khayrullin 93e2716817 feat(in-719): change api version 2 years ago
andreidekterev 1edd818afd feat(supertournaments-logo-fix): add condition for extension logo for supertournaments 2 years ago
Margarita 01786d947a feat(in-716): changed jpg to png 2 years ago
Farber Denis f9823d191f fix(#in670): header color fixes 2 years ago
Ruslan Khayrullin d9d4de4072 feat(in-621): subscriptions popup redesign 2 years ago
Margarita 63f7c01e09 feat(in-565): pr fixes 2 years ago
Margarita 0170d0fb06 feat(in-565): ads on home and match pages 2 years ago
Rakov e5552e4d8b fix(rustat): dron rustat deploy 2 years ago
andreidekterev c67bc5a22c fix(#689): add tournament info for default landing 2 years ago
Farber Denis 247a08902d fix(#in701): auth requests fix 2 years ago
andreidekterev 8c88cb7517 fix(#602): add redirect param for change password 2 years ago
Rakov 551460fc14 fix(#622): india payment phonePe 2 years ago
andreidekterev cc595acfd4 Revert "feat(#680): add statsview page" 2 years ago
andreidekterev 541a042597 Revert "feat(#680): add embed component" 2 years ago
andreidekterev 1b7a2eb111 feat(#680): add embed component 2 years ago
andreidekterev e1d82fc86e feat(#680): add statsview page 2 years ago
andreidekterev 17e8794399 Revert "feat(#680): add statsview page" 3 years ago
andreidekterev c90d4fc769 Revert "feat(#680): add embed component" 3 years ago
Ruslan Khayrullin 90f7568a2a fix(in-685): fix access to match 3 years ago
Dmitry Kosolapov home b163307892 add deploy india.insports.tv 3 years ago
Rakov 1e7ffc7366 fix(#622): annual access 3 years ago
andreidekterev cf8515fe9e feat(#680): add embed component 3 years ago
andreidekterev 3a786f97c9 feat(#680): add statsview page 3 years ago
Rakov 403690369d fix(sentry): disable sentry 3 years ago
Rakov 98bb7e24c1 fix(#593): send user id to ga 3 years ago
  1. 160
      .drone.yml
  2. 30
      Makefile
  3. 5
      package-lock.json
  4. 1
      package.json
  5. BIN
      public/clients/facr/favicon/android-chrome-192x192.png
  6. BIN
      public/clients/facr/favicon/android-chrome-512x512.png
  7. BIN
      public/clients/facr/favicon/apple-touch-icon.png
  8. BIN
      public/clients/facr/favicon/favicon-16x16.png
  9. BIN
      public/clients/facr/favicon/favicon-32x32.png
  10. BIN
      public/clients/facr/favicon/favicon.ico
  11. BIN
      public/clients/fqtv/favicon/android-chrome-192x192.png
  12. BIN
      public/clients/fqtv/favicon/android-chrome-512x512.png
  13. BIN
      public/clients/fqtv/favicon/apple-touch-icon.png
  14. BIN
      public/clients/fqtv/favicon/favicon-16x16.png
  15. BIN
      public/clients/fqtv/favicon/favicon-32x32.png
  16. BIN
      public/clients/fqtv/favicon/favicon.ico
  17. BIN
      public/clients/india/favicon/android-chrome-192x192.png
  18. BIN
      public/clients/india/favicon/android-chrome-512x512.png
  19. BIN
      public/clients/india/favicon/apple-touch-icon.png
  20. 12
      public/clients/india/favicon/browserconfig.xml
  21. BIN
      public/clients/india/favicon/favicon-16x16.png
  22. BIN
      public/clients/india/favicon/favicon-32x32.png
  23. BIN
      public/clients/india/favicon/favicon.ico
  24. 19
      public/clients/india/favicon/manifest.json
  25. BIN
      public/clients/india/favicon/mstile-144x144.png
  26. BIN
      public/clients/india/favicon/mstile-150x150.png
  27. BIN
      public/clients/india/favicon/mstile-310x310.png
  28. BIN
      public/clients/india/favicon/mstile-70x70.png
  29. 18
      public/clients/india/favicon/safari-pinned-tab.svg
  30. 1903
      public/clients/india/privacy-policy-and-statement.html
  31. 4385
      public/clients/india/terms-and-conditions.html
  32. BIN
      public/clients/lff/favicon/android-chrome-192x192.png
  33. BIN
      public/clients/lff/favicon/android-chrome-512x512.png
  34. BIN
      public/clients/lff/favicon/apple-touch-icon.png
  35. BIN
      public/clients/lff/favicon/favicon-16x16.png
  36. BIN
      public/clients/lff/favicon/favicon-32x32.png
  37. BIN
      public/clients/lff/favicon/favicon.ico
  38. 4
      public/images/matchTabs/likes.svg
  39. 23
      public/silent-refresh.html
  40. 163
      src/components/Ads/components/AdComponent/hooks.tsx
  41. 91
      src/components/Ads/components/AdComponent/index.tsx
  42. 109
      src/components/Ads/components/AdComponent/styled.tsx
  43. 79
      src/components/Ads/components/MobileAd/index.tsx
  44. 115
      src/components/Ads/components/MobileAd/styled.tsx
  45. 16
      src/components/Ads/helpers/calcMaxDurationAds.tsx
  46. 1
      src/components/Ads/helpers/index.tsx
  47. 2
      src/components/Ads/helpers/isVideo.tsx
  48. 65
      src/components/Ads/hooks.tsx
  49. 31
      src/components/Ads/index.tsx
  50. 16
      src/components/Ads/styled.tsx
  51. 59
      src/components/Ads/types.tsx
  52. 72
      src/components/Carousel/helpers/common.tsx
  53. 274
      src/components/Carousel/helpers/elements.tsx
  54. 4
      src/components/Carousel/helpers/index.tsx
  55. 12
      src/components/Carousel/helpers/mappers.tsx
  56. 44
      src/components/Carousel/helpers/math.tsx
  57. 160
      src/components/Carousel/hooks.tsx
  58. 86
      src/components/Carousel/index.tsx
  59. 25
      src/components/Carousel/styled.tsx
  60. 73
      src/components/Carousel/types.tsx
  61. 1
      src/config/clients/facr.tsx
  62. 1
      src/config/clients/fqtv.tsx
  63. 4
      src/config/clients/india.tsx
  64. 2
      src/config/clients/insports.tsx
  65. 1
      src/config/clients/instat.tsx
  66. 1
      src/config/clients/lff.tsx
  67. 2
      src/config/clients/tunisia.tsx
  68. 2
      src/config/clients/types.tsx
  69. 2
      src/config/index.tsx
  70. 33
      src/config/lexics/indexLexics.tsx
  71. 1
      src/config/lexics/landingLexics.tsx
  72. 1
      src/config/localStorageKeys.tsx
  73. 1
      src/config/pages.tsx
  74. 4
      src/config/payments.tsx
  75. 1
      src/config/queries.tsx
  76. 17
      src/config/routes.tsx
  77. 8
      src/config/userAgent.tsx
  78. 6
      src/features/AirPlay/index.tsx
  79. 32
      src/features/App/AuthenticatedApp.tsx
  80. 13
      src/features/App/index.tsx
  81. 9
      src/features/AuthServiceApp/components/ChangePassword/hooks.tsx
  82. 6
      src/features/AuthServiceApp/components/ConfirmPopup/index.tsx
  83. 2
      src/features/AuthServiceApp/config/clients/index.tsx
  84. 1
      src/features/AuthServiceApp/config/clients/types.tsx
  85. 2
      src/features/AuthServiceApp/config/lexics.tsx
  86. 10
      src/features/AuthStore/helpers.tsx
  87. 50
      src/features/AuthStore/hooks/useAuth.tsx
  88. 2
      src/features/BuyMatchPopup/components/CardStep/index.tsx
  89. 13
      src/features/BuyMatchPopup/components/ErrorStep/index.tsx
  90. 57
      src/features/BuyMatchPopup/components/IframePayment/hooks.tsx
  91. 94
      src/features/BuyMatchPopup/components/PackageMobile/index.tsx
  92. 125
      src/features/BuyMatchPopup/components/PackageMobile/styled.tsx
  93. 163
      src/features/BuyMatchPopup/components/PackageSelectionStep/index.tsx
  94. 22
      src/features/BuyMatchPopup/components/PackageSelectionStep/styled.tsx
  95. 206
      src/features/BuyMatchPopup/components/Packages/index.tsx
  96. 65
      src/features/BuyMatchPopup/components/Packages/styled.tsx
  97. 84
      src/features/BuyMatchPopup/components/PackagesList/index.tsx
  98. 230
      src/features/BuyMatchPopup/components/PackagesList/styled.tsx
  99. 74
      src/features/BuyMatchPopup/components/RegularPackage/index.tsx
  100. 140
      src/features/BuyMatchPopup/components/RegularPackage/styled.tsx
  101. Some files were not shown because too many files have changed in this diff Show More

@ -846,6 +846,7 @@ steps:
depends_on:
- make-diwansport
---
kind: pipeline
type: docker
@ -900,3 +901,162 @@ steps:
depends_on:
- make-fqtv
---
kind: pipeline
type: docker
name: deploy india.insports.tv
concurrency:
limit: 1
platform:
os: linux
arch: amd64
trigger:
ref:
- refs/heads/india.insports.tv
steps:
- name: npm-install
image: node:16-alpine
environment:
REACT_APP_STRIPE_PK:
from_secret: REACT_APP_STRIPE_PK
commands:
- apk add --no-cache make
- npm install --legacy-peer-deps
- name: make-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
---
kind: pipeline
type: docker
name: deploy tv.rustatsport.ru
concurrency:
limit: 1
platform:
os: linux
arch: amd64
trigger:
ref:
- refs/heads/tv.rustatsport.ru
steps:
- name: npm-install
image: node:16-alpine
environment:
REACT_APP_STRIPE_PK:
from_secret: REACT_APP_STRIPE_PK
commands:
- apk add --no-cache make
- npm install --legacy-peer-deps
- name: make-rustat
image: node:16-alpine
environment:
REACT_APP_STRIPE_PK:
from_secret: REACT_APP_STRIPE_PK
commands:
- apk add --no-cache make
- make rustat-prod
depends_on:
- npm-install
- name: deploy-rustat
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_rustat s3://insports-tv-rustatsport --delete
- aws cloudfront create-invalidation --distribution-id E15IFY23VM147K --paths "/*"
depends_on:
- make-rustat
---
kind: pipeline
type: docker
name: deploy insport.live
concurrency:
limit: 1
platform:
os: linux
arch: amd64
trigger:
ref:
- refs/heads/insport.live
steps:
- name: npm-install
image: node:16-alpine
environment:
REACT_APP_STRIPE_PK:
from_secret: REACT_APP_STRIPE_PK
commands:
- apk add --no-cache make
- npm install --legacy-peer-deps
- name: make-insport-live
image: node:16-alpine
environment:
REACT_APP_STRIPE_PK:
from_secret: REACT_APP_STRIPE_PK
commands:
- apk add --no-cache make
- make insport-live-prod
depends_on:
- npm-install
- name: deploy-insport-live
image: amazon/aws-cli:latest
environment:
AWS_ACCESS_KEY_ID:
from_secret: AWS_ACCESS_KEY_ID
AWS_SECRET_ACCESS_KEY:
from_secret: AWS_SECRET_ACCESS_KEY
AWS_DEFAULT_REGION:
from_secret: AWS_DEFAULT_REGION
AWS_MAX_ATTEMPTS: 10
commands:
- aws s3 sync build_insport_live s3://insports-live --delete
- aws cloudfront create-invalidation --distribution-id E1LBC88VYP6XVB --paths "/*"
depends_on:
- make-insport-live

@ -46,7 +46,7 @@ build-c: clean
build-d: clean
REACT_APP_TYPE=ott \
REACT_APP_ENV=staging \
REACT_APP_CLIENT=facr \
REACT_APP_CLIENT=insports \
REACT_APP_STAGE=test-d \
npm run build
@ -135,7 +135,7 @@ tunisia-build: clean
REACT_APP_ENV=staging \
REACT_APP_CLIENT=tunisia \
npm run build
fqtv-build: clean
REACT_APP_TYPE=ott \
REACT_APP_ENV=staging \
@ -148,6 +148,12 @@ lff-build: clean
REACT_APP_CLIENT=lff \
npm run build
rustat-build: clean
REACT_APP_TYPE=ott \
REACT_APP_ENV=staging \
REACT_APP_CLIENT=rustat \
npm run build
.PHONY: build
prod: clean
@ -210,7 +216,25 @@ fqtv-prod:
BUILD_PATH=build_fqtv \
npm run build && cp -r .well-known build_fqtv
deploy-all: prod preprod facr-prod lff-prod diwansport-prod india-prod fqtv-prod
rustat-prod:
rm -rf build_rustat && \
REACT_APP_TYPE=ott \
REACT_APP_ENV=production \
REACT_APP_CLIENT=rustat \
REACT_APP_STRIPE_PK=pk_live_51J5TEYEDSxVnTgDW5XxhC6ntKZKddXgKHq5HOCDmJTdfSKluMYCdLHOcUA3Miuy8HesxG1eS4c0dQRQpMsEHRrQL00USpu5xIq \
BUILD_PATH=build_rustat \
npm run build && cp -r .well-known build_rustat
insport-live-prod:
rm -rf build_insport_live && \
REACT_APP_TYPE=ott \
REACT_APP_ENV=staging \
REACT_APP_CLIENT=lff \
REACT_APP_STRIPE_PK=pk_live_51J5TEYEDSxVnTgDW5XxhC6ntKZKddXgKHq5HOCDmJTdfSKluMYCdLHOcUA3Miuy8HesxG1eS4c0dQRQpMsEHRrQL00USpu5xIq \
BUILD_PATH=build_insport_live \
npm run build && cp -r .well-known build_insport_live
deploy-all: prod preprod facr-prod lff-prod diwansport-prod india-prod fqtv-prod rustat-prod
test:
npm test

5
package-lock.json generated

@ -21000,6 +21000,11 @@
"resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz",
"integrity": "sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg=="
},
"react-ga": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/react-ga/-/react-ga-3.3.1.tgz",
"integrity": "sha512-4Vc0W5EvXAXUN/wWyxvsAKDLLgtJ3oLmhYYssx+YzphJpejtOst6cbIHCIyF50Fdxuf5DDKqRYny24yJ2y7GFQ=="
},
"react-inspector": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/react-inspector/-/react-inspector-5.1.1.tgz",

@ -39,6 +39,7 @@
"react": "^17.0.2",
"react-datepicker": "^3.1.3",
"react-dom": "^17.0.2",
"react-ga": "^3.3.1",
"react-query": "^3.39.3",
"react-router": "^5.2.0",
"react-router-dom": "^5.2.0",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 793 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 802 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<browserconfig>
<msapplication>
<tile>
<square70x70logo src="/mstile-70x70.png"/>
<square144x144logo src="/mstile-144x144.png"/>
<square150x150logo src="/mstile-150x150.png"/>
<square310x310logo src="/mstile-310x310.png"/>
<TileColor>#da532c</TileColor>
</tile>
</msapplication>
</browserconfig>

Binary file not shown.

After

Width:  |  Height:  |  Size: 477 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 982 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

@ -0,0 +1,19 @@
{
"name": "",
"short_name": "",
"icons": [
{
"src": "/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

@ -0,0 +1,18 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg" width="700.000000pt" height="700.000000pt" viewBox="0 0 700.000000 700.000000" preserveAspectRatio="xMidYMid meet">
<metadata>
Created by potrace 1.14, written by Peter Selinger 2001-2017
</metadata>
<g transform="translate(0.000000,700.000000) scale(0.100000,-0.100000)" fill="#000000" stroke="none">
<path d="M1499 6996 c-2 -2 -59 -6 -125 -9 -247 -12 -462 -57 -614 -129 -64
-30 -206 -123 -265 -173 -105 -89 -173 -164 -247 -270 -153 -219 -210 -411
-235 -790 -11 -161 -10 -4130 0 -4270 12 -158 20 -222 43 -331 60 -297 246
-573 515 -766 182 -130 345 -193 584 -225 22 -2 51 -6 65 -8 124 -17 518 -20
2260 -20 1773 0 2220 4 2295 19 11 2 45 7 75 10 75 9 120 17 190 36 121 32
186 61 300 129 87 53 77 46 155 109 140 114 277 283 353 437 83 166 119 330
139 630 11 160 10 4107 0 4260 -29 420 -108 642 -311 880 -175 205 -390 346
-626 412 -47 13 -107 26 -135 29 -27 4 -52 8 -55 9 -3 2 -44 6 -90 10 -47 4
-96 9 -110 13 -26 6 -4154 14 -4161 8z" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 598 B

After

Width:  |  Height:  |  Size: 884 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

After

Width:  |  Height:  |  Size: 15 KiB

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

After

Width:  |  Height:  |  Size: 2.8 KiB

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

@ -0,0 +1,163 @@
import { useEffect, useState } from 'react'
import ReactGA from 'react-ga'
import { updateAdsView } from 'requests'
import { useToggle } from 'hooks'
import { getLocalStorageItem, isMatchPage } from 'helpers'
import {
device,
COUNTRY,
} from 'config'
import { useMatchPageStore } from 'features/MatchPage/store'
import type { AdComponentType } from './index'
import { checkVideo } from '../../helpers'
import {
adsViews,
EventGA,
ViewsType,
} from '../../types'
const countryCode = getLocalStorageItem(COUNTRY)
export const useAd = ({ ad }: AdComponentType) => {
const isMatch = isMatchPage()
const [isOpenAd, setIsOpenAd] = useState(isMatch)
const [isNeedToShow, setIsNeedToShow] = useState(isMatch)
const [shownTime, setShownTime] = useState(0)
const { isFullscreen } = useMatchPageStore()
const views = getLocalStorageItem(adsViews) as ViewsType
const {
duration,
frequency,
id,
media,
name,
time_close,
} = ad
const {
close,
isOpen: isOpenCloseBtn,
open: showCloseBtn,
} = useToggle()
const isNeedBanner = Number(views?.HOME) % frequency === 0
const isVideo = checkVideo(media.url)
const currentAdsTime = duration - shownTime
useEffect(() => {
if (!isFullscreen) {
if (currentAdsTime === 0) {
setShownTime(0)
} else {
const stopWatch = setInterval(() => {
setShownTime((prev) => prev + 1)
}, 1000)
return () => clearInterval(stopWatch)
}
}
return undefined
}, [
isFullscreen,
currentAdsTime,
])
const handleClose = () => {
setIsOpenAd(false)
isMatch && setIsNeedToShow(false)
sendBannerClickEvent(EventGA.CLOSE)
}
const sendBannerClickEvent = (event: EventGA) => {
ReactGA.initialize('Advertisement')
ReactGA.event({
action: event,
category: 'Advertisement',
label: `${name}_${countryCode ?? ''}_${device}`,
value: id,
})
}
useEffect(() => {
setShownTime(0)
if (isMatch) {
const interval = setInterval(() => {
setIsNeedToShow(true)
setIsOpenAd(true)
}, frequency * 1000)
return () => clearInterval(interval)
}
setIsNeedToShow(isNeedBanner)
return setIsOpenAd(isNeedBanner)
}, [
frequency,
isNeedToShow,
views?.HOME,
isNeedBanner,
isMatch,
])
useEffect(() => {
if (isFullscreen || !isOpenAd) return undefined
const timeoutCloseAd = setTimeout(handleClose, currentAdsTime * 1000)
return () => {
clearTimeout(timeoutCloseAd)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
isNeedToShow,
isOpenAd,
isFullscreen,
currentAdsTime,
])
useEffect(() => {
close()
const timeoutCloseBtn = time_close && setTimeout(showCloseBtn, time_close * 1000)
return () => {
time_close && clearTimeout(timeoutCloseBtn)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
isNeedToShow,
isOpenAd,
views?.HOME,
])
useEffect(() => {
if (!isNeedToShow || (!isMatch && !isNeedBanner)) return
(async () => {
await updateAdsView({ adv_id: id })
})()
sendBannerClickEvent(EventGA.DISPLAY)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
id,
isNeedToShow,
isMatch,
])
return {
handleClose,
isNeedToShow,
isOpenAd,
isOpenCloseBtn,
isVideo,
sendBannerClickEvent,
}
}

@ -0,0 +1,91 @@
import type { MouseEvent } from 'react'
import { memo } from 'react'
import type { AdType } from 'requests'
import { useLexicsStore } from 'features/LexicsStore'
import { useAd } from './hooks'
import { EventGA } from '../../types'
import {
AdImg,
AdVideo,
AdWrapper,
LinkWrapper,
AdsCloseButton,
} from './styled'
export type AdComponentType = {
ad: AdType,
}
export const AdComponent = memo(({ ad }: AdComponentType) => {
const {
link,
media,
position,
} = ad
const {
handleClose,
isNeedToShow,
isOpenAd,
isOpenCloseBtn,
isVideo,
sendBannerClickEvent,
} = useAd({ ad })
const { suffix } = useLexicsStore()
const close = (e: MouseEvent<HTMLButtonElement>) => {
e.stopPropagation()
handleClose()
}
const onLinkClick = () => {
link && sendBannerClickEvent(EventGA.CLICK)
}
return (
position && isOpenAd && isNeedToShow
? (
<AdWrapper
position={position.id}
isOpenAd={isOpenAd}
>
{isOpenCloseBtn && (
<AdsCloseButton
onClick={close}
size={12}
position={position.id}
/>
)}
<LinkWrapper
href={link}
target='_blank'
rel='noreferrer'
onClick={onLinkClick}
>
{isVideo
? (
<AdVideo
muted={isVideo}
autoPlay={isVideo}
loop={isVideo}
src={media.url}
position={position.id}
/>
)
: (
<AdImg
src={media.url}
position={position.id}
alt={`name_${suffix}`}
/>
)}
</LinkWrapper>
</AdWrapper>
) : null
)
})

@ -0,0 +1,109 @@
import styled, { css } from 'styled-components/macro'
import includes from 'lodash/includes'
import { CloseButton } from 'features/PopupComponents'
import {
MATCH_ADS,
PLAYER_ADS,
VIEW_ADS,
} from '../../types'
type Props = {
position: number,
}
const header = [7, 8, 9]
const chooseStyle = (type: number) => {
switch (true) {
case VIEW_ADS.COLUMN === type:
return 'grid-row: 1 / 3; img {max-height: none;}'
case VIEW_ADS.ROW === type:
return 'grid-column: 1 / 3'
case VIEW_ADS.SQUARE === type:
return 'grid-row: 1 / 3; grid-column: 1 / 3; img {max-height: none;}'
case VIEW_ADS.SECOND_COLUMN === type:
return 'grid-column: 2 / 3; grid-row: 1 / 1'
case VIEW_ADS.SECOND_ROW === type:
return 'grid-column: 1 / 2; grid-row: 2 / 3;'
case MATCH_ADS.PLAYS_TOP === type:
return 'margin-left: 14px; height: 48px'
case MATCH_ADS.PLAYS_BOTTOM === type:
return 'grid-row: 4; margin-bottom: 12px; height: 48px;'
case PLAYER_ADS.LEFT_BOTTOM === type:
return css`
height: auto;
width: 42.4%;
bottom: 100px;
left: 20px;
`
case PLAYER_ADS.CENTER_BOTTOM === type:
return css`
height: 18.3%;
width: 81.3%;
bottom: 100px;
left: 50%;
transform: translateX(-50%);
`
case PLAYER_ADS.RIGHT === type:
return css`
height: 87.2%;
width: 18.3%;
bottom: 90px;
right: 18px;
`
case PLAYER_ADS.FULL_SCREEN === type:
return 'bottom: 0; left: 0;'
default:
return ''
}
}
export const AdImg = styled.img<Props>`
width: 100%;
min-height: ${({ position }) => (!includes(header, position) && '100%')};
max-height: ${({ position }) => (includes(header, position) ? '13rem' : '100%')};
cursor: pointer;
border-radius: 3px;
`
export const AdVideo = styled.video<Props>`
object-fit: contain;
width: 100%;
cursor: pointer;
max-height: ${({ position }) => (includes(header, position) ? '283px' : '100%')};
background-color: black;
border-radius: 3px;
`
export const AdWrapper = styled.div<Props & {isOpenAd: boolean}>`
position: ${({ position }) => (includes(PLAYER_ADS, position) ? 'absolute' : 'relative')};
width: 100%;
height: 100%;
z-index: 1;
${({ position }) => chooseStyle(position)};
display: ${({ isOpenAd }) => (isOpenAd ? '' : 'none')};
`
export const AdsCloseButton = styled(CloseButton)<Props>`
position: absolute;
right: ${({ position }) => (position === PLAYER_ADS.FULL_SCREEN ? '10px' : '0')};
top: ${({ position }) => (position === PLAYER_ADS.FULL_SCREEN ? '10px' : '0')};
background: none;
border-radius: 0;
z-index: 2;
cursor: pointer;
color: #9B9B9B;
width: 28px;
height: 28px;
:hover {
background: none;
}
`
export const LinkWrapper = styled.a``

@ -0,0 +1,79 @@
import type { MouseEvent } from 'react'
import includes from 'lodash/includes'
import type { AdType } from 'requests'
import { useLexicsStore } from 'features/LexicsStore'
import { useAd } from '../AdComponent/hooks'
import { EventGA, PLAYER_MOBILE_FULL_SCREEN } from '../../types'
import {
AdsCloseButton,
Img,
MobileAdWrapper,
Video,
} from './styled'
type MobileAdTypes = {
ad: AdType,
}
export const MobileAd = ({ ad }: MobileAdTypes) => {
const {
link,
media,
position,
} = ad
const { suffix } = useLexicsStore()
const {
handleClose,
isNeedToShow,
isOpenAd,
isOpenCloseBtn,
isVideo,
sendBannerClickEvent,
} = useAd({ ad })
const close = (e: MouseEvent<HTMLButtonElement>) => {
e.stopPropagation()
handleClose()
}
const onLinkClick = () => {
if (link) {
sendBannerClickEvent(EventGA.CLICK)
window.open(link, '_blank')
}
}
return (
position && isOpenAd && isNeedToShow ? (
<MobileAdWrapper
position={position.id}
onClick={onLinkClick}
>
{isOpenCloseBtn
&& (
<AdsCloseButton
position={position.id}
onClick={close}
size={includes(PLAYER_MOBILE_FULL_SCREEN, position.id) ? 12 : 8}
/>
)}
{isVideo
? <Video position={position.id} src={media.url} />
: (
<Img
position={position.id}
src={media.url}
alt={`name_${suffix}`}
/>
)}
</MobileAdWrapper>
) : null
)
}

@ -0,0 +1,115 @@
import styled, { css } from 'styled-components/macro'
import includes from 'lodash/includes'
import { CloseButton } from 'features/PopupComponents'
import {
MATCH_ADS,
PLAYER_MOBILE_FULL_SCREEN,
PLAYER_MOBILE_ADS,
MATCH_PAGE_MOBILE_ADS,
VIEW_ADS,
} from '../../types'
type Props = {
position: number,
}
const chooseStyle = (type: number) => {
switch (type) {
case MATCH_ADS.PLAYS_BOTTOM_MOBILE:
return css`
grid-row: 4;
margin-bottom: 12px;
height: 48px;
`
case PLAYER_MOBILE_ADS:
return css`
position: absolute;
width: 92%;
bottom: 50px;
left: 15px;
`
case PLAYER_MOBILE_FULL_SCREEN.VERTICAL_FULL_SCREEN:
case PLAYER_MOBILE_FULL_SCREEN.HORIZONTAL_FULL_SCREEN:
return css`
position: absolute;
top: 0;
left: 0;
height: 100%;
padding: 5px;
background-color: rgba(0, 0, 0, 0.7);
`
case VIEW_ADS.MOBILE_IN_COLLAPSE_FOOTER:
case VIEW_ADS.MOBILE_IN_COLLAPSE_HEADER:
return 'margin-top: 6px'
default:
return ''
}
}
export const MobileAdWrapper = styled.div<Props>`
position: relative;
width: 100%;
z-index: ${({ position }) => (includes(PLAYER_MOBILE_FULL_SCREEN, position) ? '101' : '4')};
${({ position }) => chooseStyle(position)};
`
export const AdsCloseButton = styled(CloseButton)<Props>`
position: absolute;
right: ${({ position }) => (includes(PLAYER_MOBILE_FULL_SCREEN, position) ? '-5px' : '-10px')};
top: ${({ position }) => (includes(PLAYER_MOBILE_FULL_SCREEN, position) ? '15px' : '10px')};
background: none;
border-radius: 0;
transform: translate(-50%, -50%);
z-index: 2;
color: #9B9B9B;
`
export const Img = styled.img<Props>`
border-radius: 2px;
width: 100%;
object-fit: ${({ position }) => {
switch (true) {
case position === PLAYER_MOBILE_FULL_SCREEN.VERTICAL_FULL_SCREEN:
return 'fill'
case position === MATCH_ADS.PLAYS_TOP_MOBILE:
return 'contain'
default:
return 'cover'
}
}};
height: ${({ position }) => {
switch (true) {
case position === 10:
return '50px'
case includes(MATCH_PAGE_MOBILE_ADS, position):
return '100%'
default:
return '75px'
}
}}
`
export const Video = styled.video<Props>`
max-height: 100%;
object-fit: cover;
min-width: 100%;
height: ${({ position }) => (position === 10 ? '50px' : '75px')};
border-radius: 2px;
height: ${({ position }) => {
switch (true) {
case position === 10:
return '50px'
case position === MATCH_ADS.PLAYS_TOP_MOBILE:
return '48px'
default:
return '75px'
}
}}
`

@ -0,0 +1,16 @@
import type { AdResponse, AdsListType } from 'requests'
export const calcMaxAdDurationAds = (advertisements: AdResponse) => {
const allAds = Object.values(advertisements)
const combineAds = allAds.reduce((result, currentAd) => {
result.push(...currentAd)
return result
}, [] as AdsListType)
const maxDuration = combineAds
.reduce((result, { duration }) => Math.max(result, duration), 0)
return maxDuration
}

@ -0,0 +1 @@
export * from './isVideo'

@ -0,0 +1,2 @@
const regexp = /^https?:\/\/\S+(?:mp4)$/
export const checkVideo = (url: string) => regexp.test(url)

@ -0,0 +1,65 @@
import { useMemo } from 'react'
import { useQuery } from 'react-query'
import { useRecoilState } from 'recoil'
import { isMobileDevice, querieKeys } from 'config'
import { getAds } from 'requests'
import { isMatchPage } from 'helpers/isMatchPage'
import { useLang } from 'features/LexicsStore/hooks/useLang'
import { useAuthStore } from 'features/AuthStore'
import {
DeviceType,
PageType,
} from './types'
import { calcMaxAdDurationAds } from './helpers/calcMaxDurationAds'
import { adsStore } from '../../pages/HighlightsPage/storeHighlightsAtoms'
type Props = {
matchId?: number,
sportType?: number,
tournamentId?: number,
}
export const useAds = ({
matchId,
sportType,
tournamentId,
}: Props) => {
const [ads, setAds] = useRecoilState(adsStore)
const { lang } = useLang()
const { user } = useAuthStore()
useQuery({
enabled: isMatchPage() ? (!!user && !!tournamentId) : !!user,
queryFn: async () => {
const adsList = await getAds({
client_type: isMobileDevice ? DeviceType.MOBILE : DeviceType.WEB,
language: lang,
type_id: isMatchPage() ? PageType.MATCH : PageType.HOME,
...isMatchPage() && {
matches: [{
match_id: matchId,
sport_id: sportType,
}],
tournaments: [{
sport_id: sportType,
tournament_id: tournamentId,
}],
},
})
adsList && setAds(adsList)
return adsList
},
queryKey: [querieKeys.ads, matchId],
staleTime: useMemo(() => Math.max(calcMaxAdDurationAds(ads), 60 * 1000), [ads]),
})
return {
ads,
}
}

@ -0,0 +1,31 @@
import type { AdType } from 'requests'
import { isMobileDevice } from 'config'
import type { AdsPropsType } from './types'
import { AdComponent } from './components/AdComponent'
import { MobileAd } from './components/MobileAd'
import {
HeaderWrapAd,
} from './styled'
export const HeaderAds = ({ ads }: AdsPropsType) => (
ads?.length ? (
<HeaderWrapAd column={ads?.length}>
{ads.map((ad: AdType) => (
!isMobileDevice ? (
<AdComponent
ad={ad}
key={ad.id}
/>
) : (
<MobileAd
ad={ad}
key={ad.id}
/>
)
))}
</HeaderWrapAd>
) : null
)

@ -0,0 +1,16 @@
import styled, { css } from 'styled-components/macro'
import { isMobileDevice } from 'config'
export const HeaderWrapAd = styled.div<{column: number}>`
width: 100%;
margin-bottom: 0.7rem;
display: grid;
grid-column-gap: 0.9rem;
grid-template-columns: ${({ column }) => (column > 1 ? `repeat(${column},${16.3 * 6 / column}%)` : 'repeat(1, 98.7%)')};
${isMobileDevice && css`
padding: 0 0.71rem;
grid-template-columns: none;
`}}
`

@ -0,0 +1,59 @@
import type { AdsListType } from 'requests'
export enum PageType {
HOME = 1,
MATCH = 2,
}
export enum DeviceType {
MOBILE = 'mobile',
WEB = 'web'
}
export type ViewsType = Partial<Record<keyof typeof PageType, number>>
export enum EventGA {
CLICK = 'banner_click',
CLOSE = 'banner_close',
DISPLAY = 'banner_display'
}
export enum VIEW_ADS {
ROW = 4,
COLUMN = 5,
SQUARE = 6,
SECOND_COLUMN = 2,
SECOND_ROW = 3,
MOBILE_IN_COLLAPSE_HEADER = 12,
MOBILE_IN_COLLAPSE_FOOTER = 25
}
export const HEADER_MOBILE_ADS = [10, 11]
export enum MATCH_ADS {
WATCH_TOP = 13,
PLAYS_TOP = 14,
PLAYS_BOTTOM = 15,
PLAYS_TOP_MOBILE = 16,
PLAYS_BOTTOM_MOBILE = 17,
}
export enum PLAYER_ADS {
LEFT_BOTTOM = 18,
CENTER_BOTTOM = 19,
RIGHT = 20,
FULL_SCREEN = 21,
}
export const PLAYER_MOBILE_ADS = 22
export enum PLAYER_MOBILE_FULL_SCREEN {
VERTICAL_FULL_SCREEN = 23,
HORIZONTAL_FULL_SCREEN = 24,
}
export const MATCH_PAGE_MOBILE_ADS = [16, 17, 22, 23, 24]
export type AdsPropsType = Record<'ads', AdsListType | undefined>
export const adsViews = 'adsViews'

@ -0,0 +1,72 @@
import type { Props, State } from '../types'
import {
createVariableSizingTransformationSet,
createClones,
createDefaultTransformationSet,
getElementDimensions,
getItemsCount,
getItemsOffset,
getTransitionProperty,
getTranslate3dProperty,
getItemsInSlide,
} from './elements'
import { getActiveIndex, getStartIndex } from './math'
export const calculateInitialState = (props: Props, el: HTMLElement | null): State => {
const {
activeIndex: propsActiveIndex = 0,
animationDuration = 0,
infinite = false,
variableSizing = false,
} = props
const clones = createClones(props)
const transition = getTransitionProperty()
const itemsCount = getItemsCount(props)
const itemsOffset = getItemsOffset(props)
const itemsInSlide = getItemsInSlide(itemsCount, props)
const startIndex = getStartIndex(propsActiveIndex, itemsCount)
const activeIndex = getActiveIndex({
infinite,
itemsCount,
startIndex,
})
const { width: listWidth } = getElementDimensions(el)
const transformationSet = variableSizing
? createVariableSizingTransformationSet({
el,
infinite,
listWidth,
props,
}).coords
: createDefaultTransformationSet({
children: clones,
infinite,
itemsInSlide,
listWidth,
props,
}).coords
const translate3d = getTranslate3dProperty(activeIndex, {
infinite,
itemsInSlide,
itemsOffset,
transformationSet,
variableSizing,
})
return {
activeIndex,
animationDuration,
clones,
infinite,
itemsCount,
itemsInSlide,
itemsOffset,
listWidth,
transformationSet,
transition,
translate3d,
variableSizing,
}
}

@ -0,0 +1,274 @@
import type { CSSProperties, ReactNode } from 'react'
import { Children } from 'react'
import type {
Transformations,
ItemCoords,
Props,
State,
Transition,
} from '../types'
import { mapPartialCoords, mapPositionCoords } from './mappers'
import { getShiftIndex } from './math'
export const getSlides = ({ children }: Props) => Children.toArray(children)
export const getItemsCount = (props: Props) => getSlides(props).length
export const getItemsOffset = ({ infinite }: Props) => (infinite ? 1 : 0)
export const createClones = (props: Props) => {
const slides = getSlides(props)
if (!props.infinite) return slides
const itemsCount = getItemsCount(props)
const itemsOffset = getItemsOffset(props)
const itemsInSlide = getItemsInSlide(itemsCount, props)
const cursor = Math.min(itemsInSlide, itemsCount) + itemsOffset
const clonesAfter = slides.slice(0, cursor)
const clonesBefore = slides.slice(-cursor)
if (itemsOffset && itemsInSlide === itemsCount) {
const afterOffsetClone = slides[0]
const [beforeOffsetClone] = slides.slice(-1)
clonesBefore.unshift(beforeOffsetClone)
clonesAfter.push(afterOffsetClone)
}
return clonesBefore.concat(slides, clonesAfter)
}
type CreateVariableSizingTransformationSetArgs = {
el: HTMLElement | null,
infinite: boolean,
listWidth: number,
props: Props,
}
export const createVariableSizingTransformationSet = ({
el,
infinite,
listWidth,
props,
}: CreateVariableSizingTransformationSetArgs) => {
let content = 0
let partial = true
let coords: Array<ItemCoords> = []
const { spaceBetween = 0 } = props
const children: Array<HTMLElement | Element> = Array.from(el?.children || [])
coords = children.reduce<Array<ItemCoords>>((
acc,
child,
i,
) => {
let position = 0
const previewsChildCursor = i - 1
const previewsChild = acc[previewsChildCursor]
const { width = 0 } = getElementDimensions(child?.firstChild as HTMLElement)
content += width + spaceBetween
partial = listWidth >= content
if (previewsChild) {
position = previewsChildCursor === 0
? previewsChild.width + spaceBetween
: previewsChild.width + previewsChild.position + spaceBetween
}
acc.push({ position, width })
return acc
}, [])
if (!infinite) {
if (partial) {
coords = mapPartialCoords(coords)
} else {
const position = content - listWidth
coords = mapPositionCoords(coords, position)
}
}
return {
content,
coords,
partial,
}
}
type CreateDefaultTransformationSetArgs = {
children: Array<ReactNode>,
infinite: boolean,
itemsInSlide: number,
listWidth: number,
props: Props,
}
export const createDefaultTransformationSet = ({
children,
infinite,
itemsInSlide,
listWidth,
props,
}: CreateDefaultTransformationSetArgs): Transformations => {
let content = 0
let partial = true
let coords: Array<ItemCoords> = []
const { spaceBetween = 0 } = props
const width = getItemWidth({
galleryWidth: listWidth,
itemsInSlide,
props,
})
coords = children.reduce<Array<ItemCoords>>((
acc,
_,
i,
) => {
let position = 0
const previewsChild = acc[i - 1]
content += width + spaceBetween
partial = listWidth >= content
if (previewsChild) {
position = width + spaceBetween + previewsChild.position || 0
}
acc.push({ position, width })
return acc
}, [])
if (!infinite) {
if (partial) {
coords = mapPartialCoords(coords)
} else {
const position = content - listWidth
coords = mapPositionCoords(coords, position)
}
}
return {
content,
coords,
partial,
}
}
type GetItemWidthArgs = {
galleryWidth: number,
itemsInSlide: number,
props: Props,
}
export const getItemWidth = ({
galleryWidth,
itemsInSlide,
props: { spaceBetween = 0 },
}: GetItemWidthArgs) => (itemsInSlide > 0
? (galleryWidth - spaceBetween * (itemsInSlide - 1)) / itemsInSlide
: galleryWidth)
export const getElementDimensions = (element: HTMLElement | null) => {
const { height, width } = element?.getBoundingClientRect() || { height: 0, width: 0 }
return { height, width }
}
export const getTransitionProperty = (options?: Transition): string => {
const { animationDuration = 0, animationTimingFunction = 'ease' } = options || {}
return `transform ${animationDuration}ms ${animationTimingFunction} 0ms`
}
export const getListElementStyles = (
{ translate3d }: Partial<State>,
currentStyles: CSSProperties,
): CSSProperties => {
const transform = `translate3d(${-(translate3d || 0)}px, 0, 0)`
return { ...currentStyles, transform }
}
export const getItemStyles = (index: number, state: State): CSSProperties => {
const { transformationSet } = state
const { width } = transformationSet[index] || {}
return {
width,
}
}
export const getTranslate3dProperty = (nextIndex: number, state: Partial<State>) => {
let cursor = nextIndex
const {
infinite,
itemsInSlide = 0,
itemsOffset = 0,
transformationSet = [],
} = state
if (infinite) {
cursor = nextIndex + getShiftIndex(itemsInSlide, itemsOffset)
}
return (transformationSet[cursor] || {}).position || 0
}
export const isDisplayedItem = (i = 0, state: State) => {
const {
activeIndex,
infinite,
itemsInSlide,
itemsOffset,
variableSizing,
} = state
const shiftIndex = getShiftIndex(itemsInSlide, itemsOffset)
if (variableSizing && infinite) {
return i - shiftIndex === activeIndex + itemsOffset
}
const index = activeIndex + shiftIndex
if (!infinite) {
return i >= activeIndex && i < index
}
return i >= index && i < index + itemsInSlide
}
export const getItemsInSlide = (itemsCount: number, props: Props) => {
let itemsInSlide = 1
const {
breakpoints = {},
infinite,
variableSizing,
} = props
if (variableSizing) {
return infinite ? itemsCount : itemsInSlide
}
const configKeys = Object.keys(breakpoints)
configKeys.forEach((key) => {
if (Number(key) <= window.innerWidth) {
const { items, itemsFit = 'fill' } = breakpoints[key]
itemsInSlide = itemsFit === 'contain' ? items : Math.min(items, itemsCount)
}
})
return itemsInSlide || 1
}

@ -0,0 +1,4 @@
export * from './common'
export * from './elements'
export * from './math'
export * from './mappers'

@ -0,0 +1,12 @@
import type { ItemCoords } from '../types'
export const mapPartialCoords = (coords: Array<ItemCoords>) => (
coords.map(({ width }) => ({ position: 0, width }))
)
export const mapPositionCoords = (coords: Array<ItemCoords>, position = 0) => coords.map((item) => {
if (item.position > position) {
return { ...item, position }
}
return item
})

@ -0,0 +1,44 @@
import type { ItemCoords } from '../types'
export const getShiftIndex = (itemsInSlide = 0, itemsOffset = 0) => itemsInSlide + itemsOffset
export const getStartIndex = (index = 0, itemsCount = 0) => {
if (itemsCount) {
if (index >= itemsCount) {
return itemsCount - 1
}
if (index > 0) {
return index
}
}
return 0
}
export const getActiveIndex = ({
infinite = false,
itemsCount = 0,
startIndex = 0,
}) => (infinite ? startIndex : getStartIndex(startIndex, itemsCount))
export const getUpdateSlidePositionIndex = (activeIndex: number, itemsCount: number) => {
if (activeIndex < 0) return itemsCount - 1
if (activeIndex >= itemsCount) return 0
return activeIndex
}
export const shouldRecalculateSlideIndex = (activeIndex: number, itemsCount: number) => (
activeIndex < 0 || activeIndex >= itemsCount
)
export const shouldCancelSlideAnimation = (activeIndex: number, itemsCount: number) => (
activeIndex < 0 || activeIndex >= itemsCount
)
export const getTransformationItemIndex = (
transformationSet: Array<ItemCoords> = [],
position = 0,
) => transformationSet.findIndex((item) => item.position >= Math.abs(position))

@ -0,0 +1,160 @@
import {
useRef,
useEffect,
useLayoutEffect,
type ReactNode,
} from 'react'
import { KEYBOARD_KEYS } from 'config'
import { useEventListener, useObjectState } from 'hooks'
import type { State, Props } from './types'
import * as Utils from './helpers'
import { ListItem } from './styled'
export const useCarousel = (props: Props) => {
const [state, setState] = useObjectState<State>(Utils.calculateInitialState(props, null))
const isAnimationDisabledRef = useRef(false)
const slideEndTimeoutIdRef = useRef<number | null>(null)
const listElementRef = useRef<HTMLUListElement>(null)
const rootElementRef = useRef<HTMLDivElement>(null)
const {
activeIndex,
animationDuration,
clones,
itemsCount,
itemsInSlide,
transition,
translate3d,
} = state
const {
animationTimingFunction,
infinite,
onSlideChange,
useKeyboardNavigation,
} = props
const listElementStyles = Utils.getListElementStyles({ translate3d }, { transition })
const clearSlideEndTimeout = () => {
slideEndTimeoutIdRef.current && clearTimeout(slideEndTimeoutIdRef.current)
slideEndTimeoutIdRef.current = null
}
const slidePrev = () => {
const newActiveIndex = activeIndex - 1
handleSlideTo(newActiveIndex)
}
const slideNext = () => {
const newActiveIndex = activeIndex + 1
handleSlideTo(newActiveIndex)
}
const handleUpdateSlidePosition = (index: number) => {
const newTranslate3d = Utils.getTranslate3dProperty(index, state)
const newTransition = Utils.getTransitionProperty({ animationDuration: 0 })
setState({
activeIndex: index,
transition: newTransition,
translate3d: newTranslate3d,
})
}
const handleBeforeSlideEnd = (index: number) => {
if (Utils.shouldRecalculateSlideIndex(index, itemsCount)) {
const nextIndex = Utils.getUpdateSlidePositionIndex(index, itemsCount)
handleUpdateSlidePosition(nextIndex)
}
isAnimationDisabledRef.current = false
}
const handleSlideTo = (newActiveIndex = 0) => {
if (
isAnimationDisabledRef.current
|| newActiveIndex === activeIndex
|| (!infinite && Utils.shouldCancelSlideAnimation(newActiveIndex, itemsCount))
) return
isAnimationDisabledRef.current = true
clearSlideEndTimeout()
const newTranslate3d = Utils.getTranslate3dProperty(newActiveIndex, state)
const newTransition = Utils.getTransitionProperty({
animationDuration,
animationTimingFunction,
})
onSlideChange?.(newActiveIndex)
setState({
activeIndex: newActiveIndex,
transition: newTransition,
translate3d: newTranslate3d,
})
slideEndTimeoutIdRef.current = setTimeout(
() => handleBeforeSlideEnd(newActiveIndex),
animationDuration,
)
}
const renderItem = (item: ReactNode, index: number) => {
const styles = Utils.getItemStyles(index, state)
return (
<ListItem
key={index}
style={styles}
aria-hidden={!Utils.isDisplayedItem(index, state)}
>
{item}
</ListItem>
)
}
useEventListener({
callback: (e) => {
if (!useKeyboardNavigation) return
if (e.key === KEYBOARD_KEYS.ArrowLeft) slidePrev()
if (e.key === KEYBOARD_KEYS.ArrowRight) slideNext()
},
event: 'keydown',
})
useLayoutEffect(() => {
const setInitialState = () => {
const initialState = Utils.calculateInitialState(props, listElementRef.current)
setState(initialState)
}
setInitialState()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [props.activeIndex])
useEffect(() => () => {
slideEndTimeoutIdRef.current && clearTimeout(slideEndTimeoutIdRef.current)
}, [])
return {
activeIndex,
clones,
itemsCount,
itemsInSlide,
listElementRef,
listElementStyles,
renderItem,
rootElementRef,
slideNext,
slidePrev,
}
}

@ -0,0 +1,86 @@
import { ArrowButton, Arrow } from 'features/HeaderFilters/components/DateFilter/styled'
import type { Props } from './types'
import { useCarousel } from './hooks'
import {
Wrapper,
List,
ButtonsWrapper,
} from './styled'
export * from './types'
type NavButtonProps = {
direction: 'left' | 'right',
disabled?: boolean,
onClick: () => void,
}
const NavButton = ({
direction,
disabled,
onClick,
}: NavButtonProps) => (
<ArrowButton
aria-label={direction === 'left' ? 'Previous' : 'Next'}
disabled={disabled}
onClick={onClick}
>
<Arrow direction={direction} />
</ArrowButton>
)
export const Carousel = (props: Props) => {
const {
infinite,
renderNextButton,
renderPrevButton,
} = props
const {
activeIndex,
clones,
itemsCount,
itemsInSlide,
listElementRef,
listElementStyles,
renderItem,
rootElementRef,
slideNext,
slidePrev,
} = useCarousel(props)
return (
<Wrapper ref={rootElementRef}>
<List ref={listElementRef} style={listElementStyles}>
{clones.map(renderItem)}
</List>
<ButtonsWrapper>
{renderPrevButton
? renderPrevButton({
disabled: !infinite && activeIndex === 0,
onClick: slidePrev,
})
: (
<NavButton
disabled={!infinite && activeIndex === 0}
onClick={slidePrev}
direction='left'
/>
)}
{renderNextButton
? renderNextButton({
disabled: !infinite && itemsInSlide + activeIndex === itemsCount,
onClick: slideNext,
})
: (
<NavButton
disabled={!infinite && itemsInSlide + activeIndex === itemsCount}
onClick={slideNext}
direction='right'
/>
)}
</ButtonsWrapper>
</Wrapper>
)
}

@ -0,0 +1,25 @@
import styled from 'styled-components/macro'
export const Wrapper = styled.div`
width: 100%;
margin: auto;
overflow: hidden;
`
export const List = styled.ul`
display: flex;
gap: 20px;
width: 100%;
height: 100%;
`
export const ListItem = styled.li`
flex-shrink: 0;
height: 100%;
`
export const ButtonsWrapper = styled.div`
display: flex;
justify-content: center;
align-items: center;
`

@ -0,0 +1,73 @@
import type { ReactNode } from 'react'
type RenderButtonArgs = {
disabled?: boolean,
onClick: () => void,
}
export type Transition = {
animationDuration?: number,
animationTimingFunction?: string,
}
export type Breakpoints = {
[key: string]: {
// Число элементов в слайде
items: number,
// Определяет, как элемент должен заполнять контейнер в соответствии с шириной слайда
itemsFit?: 'contain' | 'fill',
},
}
export type Transformations = {
content: number,
coords: Array<ItemCoords>,
partial: boolean,
}
export type ItemCoords = {
position: number,
width: number,
}
export type Props = {
/** Текущая позиция */
activeIndex?: number,
/** Длительность анимации */
animationDuration?: number,
/** animation-timing-function */
animationTimingFunction?: string,
/** Объект с брэкпойнтами */
breakpoints?: Breakpoints,
/** Элементы карусели */
children: ReactNode,
/** Бесконечный режим прокрутки */
infinite?: boolean,
/** Колбэк при прокрутке */
onSlideChange?: (activeIndex: number) => void,
/** Рендер-функция кнопки прокрутки вперед */
renderNextButton?: (args: RenderButtonArgs) => ReactNode,
/** Рендер-функция кнопки прокрутки назад */
renderPrevButton?: (args: RenderButtonArgs) => ReactNode,
/** Расстояние между элементами карусели */
spaceBetween?: number,
/** Использование клавиатуры для навигации */
useKeyboardNavigation?: boolean,
/** Использование произвольной ширины элементов карусели */
variableSizing?: boolean,
}
export type State = {
activeIndex: number,
animationDuration?: number,
clones: Array<ReactNode>,
infinite?: boolean,
itemsCount: number,
itemsInSlide: number,
itemsOffset: number,
listWidth: number,
transformationSet: Array<ItemCoords>,
transition: string,
translate3d: number,
variableSizing: boolean,
}

@ -31,6 +31,7 @@ export const facr: ClientConfig = {
defaultLanguage: 'cs',
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,
host: 'facr.tv',
name: ClientNames.Facr,
privacyLink: '/privacy-policy-and-statement',
requests: {

@ -17,6 +17,7 @@ export const fqtv: ClientConfig = {
defaultLanguage: 'en',
description: 'Queensland’s streamed competitions, including NPL Men, NPL Women and McDonald\'s FQPL Leagues.',
disabledPreferences: true,
host: 'fqtv.com.au',
name: ClientNames.Fqtv,
privacyLink: '/privacy-policy-and-statement?client_id=insports-ott-web',
showSearch: true,

@ -17,5 +17,9 @@ export const india: ClientConfig = {
sign: 'Rupee',
},
disabledHighlights: true,
host: 'india.insports.tv',
name: ClientNames.India,
privacyLink: '/privacy-policy-and-statement?client_id=india-ott-web',
termsLink: '/terms-and-conditions?client_id=india-ott-web',
userAccountCardsHidden: true,
}

@ -1,4 +1,5 @@
import { css } from 'styled-components/macro'
import {
ClientConfig,
ClientIds,
@ -17,6 +18,7 @@ export const insports: ClientConfig = {
defaultLanguage: 'en',
description: 'Live sports streaming platform. Football, basketball, ice hockey and more. Access to various player playlists and game highlights. Multiple subscription options. Available across all devices.',
disabledPreferences: true,
host: 'insports.tv',
name: ClientNames.Insports,
privacyLink: '/privacy-policy-and-statement?client_id=insports-ott-web',
showSearch: true,

@ -17,6 +17,7 @@ export const instat: ClientConfig = {
defaultLanguage: 'en',
description: 'Live sports streaming platform. Football, basketball, ice hockey and more. Access to various player playlists and game highlights. Multiple subscription options. Available across all devices.',
disabledPreferences: true,
host: 'instat.tv',
name: ClientNames.Instat,
privacyLink: '/privacy-policy-and-statement',
showSearch: true,

@ -18,6 +18,7 @@ export const lff: ClientConfig = {
description: 'Latvijas Futbola federācija (LFF) pašmāju futbola spēļu tiešraižu, apskatu un ierakstu platforma.',
disabledHighlights: true,
disabledPreferences: true,
host: 'tv.lff.lv',
name: ClientNames.Lff,
privacyLink: '/privacy-policy-and-statement',
showSearch: true,

@ -25,6 +25,7 @@ export const tunisia: ClientConfig = {
disabledFilters: true,
disabledHighlights: true,
disabledPreferences: true,
host: 'diwansport.net',
name: ClientNames.Tunisia,
privacyLink: '/privacy-policy-and-statement?client_id=insports-ott-web',
showSearch: true,
@ -60,4 +61,5 @@ export const tunisia: ClientConfig = {
},
termsLink: '/terms-and-conditions?client_id=insports-ott-web',
title: 'Diwan Sport - The home of Tunisian Ligue Professionnelle 1',
userAccountCardsHidden: true,
}

@ -42,6 +42,7 @@ export type ClientConfig = {
disabledFilters?: boolean,
disabledHighlights?: boolean,
disabledPreferences?: boolean,
host: string,
name: ClientNames,
privacyLink: string,
requests?: Record<ProcedureName, RequestParameters>,
@ -64,5 +65,6 @@ export type ClientConfig = {
},
termsLink: string,
title: string,
userAccountCardsHidden?: boolean,
userAccountLinksDisabled?: boolean,
}

@ -12,3 +12,5 @@ export * from './userAgent'
export * from './queries'
export * from './keyboardKeys'
export * from './clients'
export * from './localStorageKeys'
export * from './payments'

@ -90,11 +90,16 @@ const confirmPopup = {
const buyMatchPopupLexics = {
add: 15075,
adding_card: 15074,
all_away_games_of: 20186,
all_games_of: 20182,
all_home_games_of: 20185,
auto_renewal: 16624,
best_choice: 20175,
buy_for: 14095,
buy_subscription: 13565,
cancel_anytime: 20171,
change_card: 13564,
choose_subscription: 13563,
choose_subscription: 20174,
completed: 14072,
description_all_season_matches: 15069,
description_all_team_matches: 15070,
@ -105,7 +110,11 @@ const buyMatchPopupLexics = {
for_month: 13561,
for_view: 15064,
for_year: 13562,
how_to_watch: 20199,
in: 20183,
in_season: 20184,
next_choose: 15156,
one_off_payment: 20172,
pass_league: 15065,
pass_match_access: 15067,
pass_team: 15066,
@ -114,8 +123,10 @@ const buyMatchPopupLexics = {
pay: 15073,
payment: 14096,
payment_confirmation: 14094,
per_month: 13573,
per_month: 20173,
per_year: 13574,
purchase: 12623,
subscription: 15604,
subscription_done: 2668,
success_subscription: 14097,
}
@ -144,6 +155,11 @@ const newDevicePopup = {
}
export const indexLexics = {
'1st_half': 1031,
'2nd_half': 1032,
'3rd_half': 20262,
'4th_half': 20263,
'5th_half': 20264,
add_to_favorites: 14967,
add_to_favorites_error: 12943,
all_competitions: 17926,
@ -159,7 +175,9 @@ export const indexLexics = {
cm: 817,
features: 13051,
football: 6958,
ft: 20261,
full_game: 13028,
full_time: 20258,
futsal: 17670,
game: 9680,
game_finished: 13026,
@ -175,9 +193,11 @@ export const indexLexics = {
hide_score: 12982,
highlights: 13033,
hockey: 6959,
ht: 20260,
interview: 13031,
kg: 652,
kickoff_in: 13027,
likes: 16628,
live: 13024,
loading: 3527,
logout: 4306,
@ -188,11 +208,14 @@ export const indexLexics = {
match_status_soon: 12986,
match_video: 13025,
month_title: 2202,
my_likes: 20254,
no_match_access_body: 13419,
no_match_access_title: 13418,
player: 14975,
players: 164,
players_video: 13032,
pm: 20259,
pre_match: 20257,
privacy_policy_and_statement: 15404,
round_highilights: 13050,
save: 828,
@ -201,15 +224,21 @@ export const indexLexics = {
sport: 12993,
team: 14973,
terms_and_conditions: 15738,
timeline_title: 20266,
to_home: 13376,
total_likes: 20253,
tournament: 14974,
upcoming: 17925,
user_account: 12928,
user_liked_this: 20267,
users_liked_this: 20255,
volleyball: 9761,
watch_from_beginning: 13021,
watch_from_last_pause: 13022,
watch_now: 13020,
week_title: 6584,
you_and: 20256,
you_liked_this: 20265,
...filterPopup,
...confirmPopup,

@ -3,6 +3,7 @@ export const landingLexics = {
inactive_button: 20083,
inactive_description_1: 20084,
inactive_description_2: 20086,
inactive_period: 801,
inactive_title_1: 20087,
inactive_title_2: 20088,
}

@ -0,0 +1 @@
export const COUNTRY = 'COUNTRY'

@ -7,6 +7,7 @@ export const PAGES = {
mailings: '/useraccount/mailings',
match: '/matches',
player: '/players',
subscriptions: '/subscriptions',
team: '/teams',
thanksForSubscribe: '/thanks-for-subscription',
tournament: '/tournaments',

@ -3,7 +3,7 @@ import { ClientNames } from './clients/types'
export enum PaymentSystem {
PagBrazil = 'pag_brasil',
Paymee = 'paymee',
Paytm = 'paytm',
PhonePe = 'phonePe',
Stripe = 'stripe'
}
@ -20,7 +20,7 @@ type PaymentsType = {
export const payments: PaymentsType = {
[ClientNames.Tunisia]: PaymentSystem.Paymee,
[ClientNames.Brasil]: PaymentSystem.PagBrazil,
[ClientNames.India]: PaymentSystem.Paytm,
[ClientNames.India]: PaymentSystem.PhonePe,
[ClientNames.Insports]: PaymentSystem.Stripe,
[ClientNames.Instat]: PaymentSystem.Stripe,
[ClientNames.Facr]: PaymentSystem.Stripe,

@ -1,4 +1,5 @@
export const querieKeys = {
ads: 'ads',
liveMatchScores: 'liveMatchScores',
matchScore: 'matchScore',
sportsList: 'sportsList',

@ -5,15 +5,18 @@ import { ENV, isProduction } from './env'
export const APIS = {
preproduction: {
api: 'https://api.insports.tv',
auth: 'https://auth.insports.tv',
auth: 'https://api.auth.insports.tv',
auth_old: 'https://auth.insports.tv',
},
production: {
api: 'https://api.insports.tv',
auth: 'https://auth.insports.tv',
auth: 'https://api.auth.insports.tv',
auth_old: 'https://auth.insports.tv',
},
staging: {
api: 'https://api.test.insports.tv',
auth: 'https://auth.test.insports.tv',
auth: 'https://api.auth.test.insports.tv',
auth_old: 'https://auth.test.insports.tv',
},
}
@ -41,13 +44,21 @@ const PAYMENT_APIS = {
staging: 'https://pay.test.insports.tv',
}
const LIKES_API = {
preproduction: 'wss://ws.insports.tv/v1/events',
production: 'wss://ws.insports.tv/v1/events',
staging: 'wss://ws-test.insports.tv/v1/events',
}
const env = isProduction ? ENV : readSelectedApi() ?? ENV
export const VIEWS_API = VIEWS_APIS[env]
export const AUTH_SERVICE = APIS[env].auth
export const AUTH_SERVICE_OLD = APIS[env].auth_old
export const API_ROOT = APIS[env].api
export const DATA_URL = `${API_ROOT}/data`
export const URL_AWS = 'https://cf-aws.insports.tv'
export const STATS_API_URL = STATS_APIS[env]
export const ADS_API_URL = ADS_APIS[env]
export const PAYMENT_API_URL = PAYMENT_APIS[env]
export const LIKES_API_URL = LIKES_API[env]

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

@ -1,8 +1,6 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { useEffect } from 'react'
import includes from 'lodash/includes'
import { usePageParams } from 'hooks/usePageParams'
import { API_ROOT } from 'config'
@ -19,9 +17,7 @@ export const AirPlay = ({ videoRef }: Props) => {
const { profileId: matchId, sportType } = usePageParams()
useEffect(() => {
const baseUrl = includes(videoRef?.current?.src, '.m3u8')
? `${API_ROOT}/video/chromecast/stream/${sportType}/${matchId}.m3u8?access_token=${readToken()}`
: videoRef?.current?.src!
const baseUrl = `${API_ROOT}/v1/broadcasts/${sportType}/${matchId}/master.m3u8?access_token=${readToken()}`
const video = videoRef.current!
const airPlayBtn = document.getElementById('airPlay')!

@ -9,9 +9,12 @@ import {
import { RecoilRoot } from 'recoil'
import { indexLexics } from 'config/lexics/indexLexics'
import { isProduction } from 'config/env'
import { PAGES } from 'config/pages'
import { client } from 'config/clients'
import {
// client,
PAGES,
isProduction,
isMobileDevice,
} from 'config'
import { StripeElements } from 'features/StripeElements'
import { useLexicsConfig } from 'features/LexicsStore'
@ -19,9 +22,12 @@ import { ExtendedSearchStore } from 'features/ExtendedSearchPage'
import { MatchSwitchesStore } from 'features/MatchSwitches'
import { UserFavoritesStore } from 'features/UserFavorites/store'
import { MatchPopup, MatchPopupStore } from 'features/MatchPopup'
import { BuyMatchPopup, BuyMatchPopupStore } from 'features/BuyMatchPopup'
import { PreferencesPopup, PreferencesPopupStore } from 'features/PreferencesPopup'
import { TournamentsPopup } from 'features/TournamentsPopup'
import { BuyMatchPopupStore } from 'features/BuyMatchPopup'
import {
// PreferencesPopup,
PreferencesPopupStore,
} from 'features/PreferencesPopup'
// import { TournamentsPopup } from 'features/TournamentsPopup'
import { TournamentPopupStore } from 'features/TournamentsPopup/store'
import { CardsStore } from 'features/CardsStore'
import { NoNetworkPopup, NoNetworkPopupStore } from 'features/NoNetworkPopup'
@ -41,6 +47,7 @@ const HighlightsPage = lazy(() => import('pages/HighlightsPage'))
const ThanksPage = lazy(() => import('pages/ThanksPage'))
const Mailings = lazy(() => import('pages/Mailings'))
const FailedPaymeePage = lazy(() => import('pages/FailedPaymeePage'))
const SubscriptionsPage = lazy(() => import('pages/SubscriptionsPage'))
export const AuthenticatedApp = () => {
useSportList()
@ -59,8 +66,12 @@ export const AuthenticatedApp = () => {
<BuyMatchPopupStore>
<NoNetworkPopupStore>
<MatchPopup />
<BuyMatchPopup />
{ client.name === 'facr' ? <TournamentsPopup /> : <PreferencesPopup /> }
{/* временно отключено по задаче IN-730 */}
{/* {
client.name === 'facr'
? <TournamentsPopup />
: <PreferencesPopup />
} */}
<NoNetworkPopup />
{/* в Switch как прямой children
можно рендерить только Route или Redirect */}
@ -98,6 +109,11 @@ export const AuthenticatedApp = () => {
<Route path={`${PAGES.landing}`}>
<Landings />
</Route>
{isMobileDevice && (
<Route path={PAGES.subscriptions}>
<SubscriptionsPage />
</Route>
)}
<Redirect to={PAGES.home} />
</Switch>
{!isProduction && <SystemSettings />}

@ -32,13 +32,24 @@ setClientTitleAndDescription(client.title, client.description)
const Main = () => {
const [isToken, setIsToken] = useState(false)
const { userInfo } = useAuthStore()
const { user, userInfo } = useAuthStore()
const queryClient = new QueryClient()
useEffect(() => {
if (userInfo) readToken() && setIsToken(true)
}, [userInfo])
// отправляем идентификаторы пользователей в гугл аналитику
useEffect(() => {
if (user) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(window as any)?.dataLayer.push({
event: 'userData',
userId: user.profile?.sub,
})
}
}, [user])
// имеется действующий токен
return isToken ? (
<ErrorBoundary>

@ -8,12 +8,14 @@ import { useHistory } from 'react-router'
import { useAuthFields } from 'features/AuthServiceApp/hooks/useAuthFields'
import { changePassword } from 'features/AuthServiceApp/requests/changePassword'
import { clients } from 'features/AuthServiceApp/config/clients'
import { useParamsUrl } from '../../hooks/useParamsUrl'
export const useChangePasswordForm = () => {
const { client_id } = useParamsUrl()
const { client_id, redirect_uri } = useParamsUrl()
const history = useHistory()
const [error, setError] = useState('')
const [modalOpen, setModalOpen] = useState<boolean>(false)
const [isFetching, setIsFetching] = useState(false)
@ -36,9 +38,12 @@ export const useChangePasswordForm = () => {
setIsFetching(false)
}
}
const host = String(redirect_uri || clients[client_id].host)
const redirectUrl = `/authorize?client_id=${client_id}&redirect_uri=${encodeURIComponent(host)}`
const modalButtonClick = () => {
history.push(`/authorize?client_id=${client_id}`)
history.push(redirectUrl)
}
const onPasswordChange = ({

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

@ -8,7 +8,7 @@ import { india } from './india'
import { tunisia } from './tunisia'
import { fqtv } from './fqtv'
const clients = {
export const clients = {
[ClientIds.Facr]: facr,
[ClientIds.Fqtv]: fqtv,
[ClientIds.Instat]: instat,

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

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

@ -54,11 +54,11 @@ const redirectUrl = () => {
// @ts-ignore
|| Boolean(clientsForRedirect[client.name])
):
return `${window.origin}/redirect`
return `${window.origin}`
case (ENV === 'staging' || ENV === 'preproduction'):
return `https://${stageENV}.insports.tv/redirect`
return `https://${stageENV}.insports.tv`
default:
return `https://${clientName}.tv/redirect`
return `https://${clientName}.tv`
}
}
@ -76,4 +76,6 @@ export const getClientSettings = (): Settings => ({
userStore: new WebStorageStateStore({ store: window.localStorage }),
})
export const needCheckNewDeviсe = client.name === 'instat' || client.name === 'insports'
export const needCheckNewDeviсe = client.name === ClientNames.Instat
|| client.name === ClientNames.Insports
|| client.name === ClientNames.India

@ -23,6 +23,11 @@ import {
readToken,
setCookie,
removeCookie,
isMatchPage,
REFRESH_TOKEN_KEY,
removeRefreshToken,
writeRefreshToken,
readRefreshToken,
} from 'helpers'
import {
@ -76,6 +81,7 @@ export const useAuth = () => {
userManager.signoutRedirect({ post_logout_redirect_uri: urlWithLang })
})
removeToken()
removeRefreshToken()
if (key !== 'saveToken') {
removeCookie('access_token')
}
@ -157,10 +163,15 @@ export const useAuth = () => {
}
}
const signinRedirectCallback = useCallback(() => {
const signinRedirectCallback = useCallback((refreshToken: string | null) => {
setPage(history.location.pathname)
userManager.signinRedirectCallback()
.then((loadedUser) => {
storeUser(loadedUser)
if (refreshToken) writeRefreshToken(refreshToken)
queryParamStorage.clear()
if (page.includes(PAGES.useraccount)) {
history.push(PAGES.home)
@ -171,9 +182,8 @@ export const useAuth = () => {
markUserLoaded()
setPage('')
setSearch('')
// }
}).catch(login)
// eslint-disable-next-line react-hooks/exhaustive-deps
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
login,
storeUser,
@ -181,9 +191,18 @@ export const useAuth = () => {
])
useEffect(() => {
const isRedirectedBackFromAuthProvider = history.location.pathname === '/redirect'
isRedirectedBackFromAuthProvider ? signinRedirectCallback() : checkUser()
// eslint-disable-next-line react-hooks/exhaustive-deps
const urlSearch = new URLSearchParams(history.location.search)
const searchToken = urlSearch.get('access_token')
const searchRefToken = urlSearch.get('id_token')
const searchExp = urlSearch.get('expires_in')
const refreshToken = urlSearch.get(REFRESH_TOKEN_KEY)
const isRedirectedBackFromAuthProvider = Boolean(searchToken && searchRefToken && searchExp)
isRedirectedBackFromAuthProvider
? signinRedirectCallback(refreshToken)
: checkUser()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
checkUser,
signinRedirectCallback,
@ -213,7 +232,7 @@ export const useAuth = () => {
}, [reChekNewDevice])
useEffect(() => {
if (!needCheckNewDeviсe && !user) return undefined
if (!needCheckNewDeviсe || !user) return undefined
const startCheckDevice = setInterval(checkNewDevice, 20000)
isNewDeviceLogin && clearInterval(startCheckDevice)
return () => clearInterval(startCheckDevice)
@ -223,6 +242,7 @@ export const useAuth = () => {
checkNewDevice,
isNewDeviceLogin,
setIsNewDeviceLogin,
user,
])
duel.channel('active_page') // поле в LS, определяющее активность вкладки
@ -232,7 +252,13 @@ export const useAuth = () => {
// библиотека oidc-client не поддерживает обновление токена только на 1 вкладке
// @ts-ignore
if (window.isMaster()) {
userManager.signinSilent().catch(logout)
// safari ограничивает доступ к куке через крос доменные запросы
// передаем рефреш токен через квери параметры
userManager.signinSilent({
extraQueryParams: {
refresh_token: readRefreshToken(),
},
}).catch(logout)
}
}
// если запросы вернули 401 | 403
@ -275,6 +301,14 @@ export const useAuth = () => {
readToken() && fetchUserInfo()
}, [fetchUserInfo, user])
// временно отрубили возможность смотреть матчи вирт. юзерам
// в связи с переездом бэка
useEffect(() => {
if (userInfo && isMatchPage() && +userInfo.email < 0) {
logout()
}
}, [logout, userInfo])
const auth = useMemo(() => ({
fetchUserInfo,
isFromLanding,

@ -51,7 +51,7 @@ export const CardStep = ({
return (
<Wrapper width={642}>
<Header>
<ButtonPrevious onClick={goBack}>
<ButtonPrevious aria-label='Back' onClick={goBack}>
{isHighlightsPage ? '' : <Arrow direction='left' />}
</ButtonPrevious>
<HeaderTitle>

@ -1,3 +1,7 @@
import { useHistory } from 'react-router-dom'
import { isMobileDevice } from 'config'
import { T9n } from 'features/T9n'
import { Header, HeaderTitle } from 'features/PopupComponents'
@ -16,6 +20,13 @@ export const ErrorStep = () => {
error: paymentError,
} = useBuyMatchPopupStore()
const history = useHistory()
const handleButtonclick = () => {
close()
isMobileDevice && history.goBack()
}
return (
<Wrapper width={369}>
<Header height={24}>
@ -29,7 +40,7 @@ export const ErrorStep = () => {
</ResultText>
</Body>
<Footer>
<SmallButton onClick={close}>
<SmallButton onClick={handleButtonclick}>
Ок
</SmallButton>
</Footer>

@ -2,21 +2,27 @@ import {
MouseEvent,
useCallback,
useEffect,
useMemo,
useState,
} from 'react'
import isNumber from 'lodash/isNumber'
import { PAGES, ProfileTypes } from 'config'
import { ClientNames } from 'config/clients/types'
import { payments, PaymentSystem } from 'config/payments'
import isNumber from 'lodash/isNumber'
import { useLexicsStore } from 'features/LexicsStore'
import { useBuyMatchPopupStore } from 'features/BuyMatchPopup/store'
import { getProfileUrl } from 'features/ProfileLink/helpers'
import { SubscriptionType } from 'features/BuyMatchPopup/types'
import { getMatchInfo } from 'requests/getMatchInfo'
import { getPaymentUrl } from 'requests/getPaymentUrl'
import {
getPaymentOTTUrl,
getPaymentPayUrl,
getMatchInfo,
SubscriptionAction,
} from 'requests'
import { redirectToUrl } from 'helpers'
@ -49,6 +55,7 @@ export const useIframePayment = ({
nameLexic,
originalObject,
pass,
type,
} = selectedPackage
const teams = isNumber(nameLexic) ? translate(String(nameLexic)) : name
@ -60,6 +67,13 @@ export const useIframePayment = ({
sportType,
})
const defaultAction: SubscriptionAction = useMemo(() => {
if (type === SubscriptionType.Month) {
return SubscriptionAction.CreateSubscription
}
return SubscriptionAction.OnePayment
}, [type])
const closePopup = useCallback(async (e?: MouseEvent) => {
e?.stopPropagation()
@ -78,25 +92,25 @@ export const useIframePayment = ({
}
}, [close, error, id, matchLink, setIsOpenIframe, sportType])
const paymentRequest = async () => {
const paymentRequestOTT = async () => {
let url_cancel
let url_return
let action: Parameters<typeof getPaymentUrl>[0]['action']
let action: SubscriptionAction
switch (paymentSystem) {
case PaymentSystem.Paymee:
url_cancel = `${window.origin}/failed-paymee`
url_cancel = `${window.origin}${PAGES.failedPaymee}`
url_return = null
// paymee не умеет работать с подписками
action = 'one_payment'
action = SubscriptionAction.OnePayment
break
default:
url_return = `${window.location.origin}${PAGES.thanksForSubscribe}`
action = pass === 'pass_match_access' ? 'one_payment' : 'create_subscription'
action = defaultAction
break
}
const payment: ResponsePaymentArray = await getPaymentUrl({
const payment: ResponsePaymentArray = await getPaymentOTTUrl({
action,
item: originalObject,
product_name: `${pack} ${teams}`,
@ -107,6 +121,19 @@ export const useIframePayment = ({
setSrc(payment?.url || '')
}
// новое апи для оплаты, в будущем все платежки переедут на него
// делаем оплату на новой вкладке, а не через iframe
const paymentRequestPay = async () => {
const payment = await getPaymentPayUrl({
item: {
...originalObject,
},
url_return: `${window.location.origin}${matchLink}`,
})
redirectToUrl(payment.url)
}
if (paymentSystem === payments[ClientNames.Brasil]) {
// eslint-disable-next-line
window.onmessage = function (event) {
@ -116,6 +143,7 @@ export const useIframePayment = ({
}
}
// отслеживание оплаты для Paymee
useEffect(() => {
let interval: ReturnType<typeof setInterval>
let timeout: ReturnType<typeof setTimeout>
@ -154,7 +182,14 @@ export const useIframePayment = ({
if (open) {
(async () => {
try {
await paymentRequest()
switch (paymentSystem) {
case PaymentSystem.PhonePe:
await paymentRequestPay()
break
default:
await paymentRequestOTT()
break
}
} catch (err) {
setError('error_payment_unsuccessful')
}

@ -0,0 +1,94 @@
import { useState, type MouseEvent } from 'react'
import { useToggle } from 'hooks'
import type { MatchPackage } from 'features/BuyMatchPopup/types'
import { SubscriptionType } from 'features/BuyMatchPopup/types'
import { Price } from 'features/Price'
import { T9n } from 'features/T9n'
import { useBuyMatchPopupStore } from 'features/BuyMatchPopup/store'
import { ArrowLoader } from 'features/ArrowLoader'
import { usePackage } from '../RegularPackage/usePackage'
import {
Wrapper,
Header,
Title,
Button,
Content,
Description,
Body,
Border,
SubscriptionTypeText,
BestChoice,
} from './styled'
type Props = {
buttonId?: string,
buttonLexic?: string,
matchPackage: MatchPackage,
onButtonClick?: (e: MouseEvent<HTMLButtonElement>, matchPackage?: MatchPackage) => void,
}
export const PackageMobile = ({
buttonId,
buttonLexic,
matchPackage,
onButtonClick,
}: Props) => {
const {
matchPackages,
setSelectedPackage,
} = useBuyMatchPopupStore()
const isSinglePackage = matchPackages.length === 1
const { isOpen, toggle } = useToggle(isSinglePackage)
const [loader, setLoader] = useState(false)
const { firstDescription, priceTextTopLexic } = usePackage(matchPackage)
const handleButtonClick = (e: MouseEvent<HTMLButtonElement>) => {
setSelectedPackage(matchPackage)
onButtonClick?.(e, matchPackage)
setLoader(true)
}
return (
<Wrapper>
<Header
isOpen={isOpen}
onClick={isSinglePackage ? undefined : toggle}
>
<Title t={matchPackage.originalObject.sub.lexic1} />
<Price
amount={matchPackage.originalObject.price}
currency={matchPackage.currency}
perPeriod={matchPackage.isMonthSubscription ? `per_${SubscriptionType.Month}` : undefined}
/>
</Header>
{isOpen && <Border />}
<Content isOpen={isOpen}>
<Body>
<Description>
{firstDescription}
</Description>
<Description>
<T9n t={matchPackage.originalObject.sub.lexic3} />
</Description>
{matchPackage.isMainPackage && (
<BestChoice>
<T9n t='best_choice' />
</BestChoice>
)}
<SubscriptionTypeText t={priceTextTopLexic} />
</Body>
<Button id={buttonId} onClick={handleButtonClick}>
{loader
? <ArrowLoader disabled />
: <T9n t={buttonLexic || ''} />}
</Button>
</Content>
</Wrapper>
)
}

@ -0,0 +1,125 @@
import styled, { css } from 'styled-components/macro'
import { T9n } from 'features/T9n'
import { ButtonSolid } from 'features/Common'
import { PriceDetails, PriceAmount } from 'features/Price/styled'
export const Wrapper = styled.div`
width: 100%;
border-radius: 2px 2px 0px 0px;
background-color: #414141;
box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.3);
`
type HeaderProps = {
isOpen?: boolean,
}
export const Header = styled.div<HeaderProps>`
position: relative;
display: flex;
justify-content: space-between;
height: 52px;
align-items: center;
padding: 0 15px 0 20px;
${PriceAmount} {
font-size: 24px;
position: absolute;
right: 46px;
top: 50%;
translate: 0 -50%;
}
${PriceDetails} {
position: absolute;
top: 12px;
left: calc(100% - 48px);
}
${({ isOpen }) => (isOpen
? ''
: css`
${PriceDetails}, ${PriceAmount}, ${Title} {
color: #D9D9D9;
}
`)}
`
export const Border = styled.div`
height: 0.66px;
background: linear-gradient(90deg, transparent 0%, #656565 50%, transparent 100%);
`
export const Title = styled(T9n)`
max-width: calc(100% - 112px);
font-weight: 600;
font-size: 14px;
color: ${({ theme }) => theme.colors.white};
overflow: hidden;
`
type ContentProps = {
isOpen?: boolean,
}
export const Content = styled.div<ContentProps>`
height: ${({ isOpen }) => (isOpen ? 'auto' : 0)};
`
export const Body = styled.div`
position: relative;
padding: 8px 24px 30px;
background-color: #414141;
`
export const Description = styled.p`
width: calc(100% - 90px);
margin-bottom: 15px;
font-weight: 500;
font-size: 12px;
line-height: 16px;
color: #C7C7C7;
:empty, :has(span:empty) {
display: none;
}
:last-of-type {
margin-bottom: 0;
}
`
export const Button = styled(ButtonSolid)`
width: 100%;
height: 48px;
`
export const SubscriptionTypeText = styled(T9n)`
position: absolute;
right: 16px;
bottom: 12px;
font-size: 12px;
font-weight: 600;
color: ${({ theme }) => theme.colors.white};
`
export const BestChoice = styled.div`
position: absolute;
bottom: 40px;
right: 0;
display: flex;
align-items: center;
justify-content: flex-end;
width: 108px;
height: 20px;
padding-right: 16px;
border-radius: 1px 0px 0px 1px;
font-weight: 600;
font-size: 12px;
font-variant: all-small-caps;
letter-spacing: 0.03em;
color: #464646;
background: linear-gradient(270deg, rgba(253, 253, 254, 0.8) 0%, rgba(253, 253, 254, 0) 97.42%);
backdrop-filter: blur(3px);
`

@ -1,26 +1,34 @@
import {
Fragment,
useEffect,
useState,
MouseEvent,
useMemo,
} from 'react'
import { useHistory } from 'react-router-dom'
import isNull from 'lodash/isNull'
import { MDASH } from 'config'
import { payments, CountryCode } from 'config/payments'
import { client } from 'config/clients'
import {
isMobileDevice,
client,
payments,
CountryCode,
} from 'config'
import { CountryCodeType, getCountryCode } from 'requests/getCountryCode'
import type { CountryCodeType } from 'requests/getCountryCode'
import { getCountryCode } from 'requests/getCountryCode'
import { CloseButton, HeaderActions } from 'features/PopupComponents'
import { T9n } from 'features/T9n'
import { Name } from 'features/Name'
import { useCardsStore } from 'features/CardsStore'
import { ArrowLoader } from 'features/ArrowLoader'
import { Arrow } from 'features/HeaderFilters/components/DateFilter/styled'
import { useAuthStore } from 'features/AuthStore'
import { Arrow } from 'features/HeaderFilters/components/DateFilter/styled'
import type { MatchPackage } from 'features/BuyMatchPopup/types'
import { ClientNames } from 'config/clients/types'
import { ChooseSub, Footer } from './styled'
import { IframePayment } from '../IframePayment'
@ -30,11 +38,9 @@ import { Packages } from '../Packages'
import {
Wrapper,
Body,
Footer,
Button,
ButtonPrevious,
Header,
HeaderTitle,
ButtonPrevious,
} from '../../styled'
export const PackageSelectionStep = () => {
@ -53,18 +59,29 @@ export const PackageSelectionStep = () => {
const {
close,
disabledBuyBtn,
goBack,
hasPreviousStep,
lastSelectedPackage,
loader,
match,
matchPackages,
onBuyClick,
selectedPackage,
selectedSubscription,
setDisabledBuyBtn,
setLastSelectedPackage,
} = useBuyMatchPopupStore()
const { defaultCard } = useCardsStore()
const history = useHistory()
const hasCard = Boolean(defaultCard)
const buttonId = hasCard ? 'purchase_buy' : 'purchase_next'
const buttonLexic = hasCard ? 'buy_subscription' : 'next_choose'
const hasOnlyOneSubscription = matchPackages.length === 1
const titleLexic = hasOnlyOneSubscription ? 'how_to_watch' : 'choose_subscription'
useEffect(() => {
getUserCountry()
if (isNull(cards)) {
@ -73,20 +90,25 @@ export const PackageSelectionStep = () => {
}, [cards, fetchCards])
const paymentSystem = useMemo(() => {
switch (countryCode?.country_code) {
case CountryCode.BR:
switch (true) {
case countryCode?.country_code === CountryCode.BR:
return payments.brasil
case CountryCode.TN:
case countryCode?.country_code === CountryCode.TN:
return payments.tunisia
case countryCode?.country_code === CountryCode.IN:
case client.name === ClientNames.India:
return payments.india
default:
return payments[client.name]
}
}, [countryCode])
const isIframePayment = useMemo(() => {
switch (countryCode?.country_code) {
case CountryCode.BR:
case CountryCode.TN:
switch (true) {
case countryCode?.country_code === CountryCode.BR:
case countryCode?.country_code === CountryCode.TN:
case countryCode?.country_code === CountryCode.IN:
case client.name === ClientNames.India:
return true
default:
return false
@ -99,65 +121,86 @@ export const PackageSelectionStep = () => {
getCountryCode().then(setCountryCode)
}
const onHandleClick = (e?: MouseEvent<HTMLButtonElement>) => {
cards?.length
&& lastSelectedPackage === selectedPackage?.id
&& setDisabledBuyBtn(true)
if (isIframePayment) {
setIsOpenIframe(true)
const onHandleClick = (e: MouseEvent<HTMLButtonElement>, matchPackage?: MatchPackage) => {
if (user) {
cards?.length
&& lastSelectedPackage === selectedPackage?.id
&& setDisabledBuyBtn(true)
if (isIframePayment) {
setIsOpenIframe(true)
} else {
onBuyClick(e, matchPackage)
}
setLastSelectedPackage(selectedPackage?.id || '')
} else {
onBuyClick(e)
setSearch(window.location.search)
logout('saveToken')
}
setLastSelectedPackage(selectedPackage?.id || '')
}
return (
<Wrapper>
if (isMobileDevice) {
const handleBackClick = () => {
close()
history.goBack()
}
<Header>
{hasPreviousStep && (
<HeaderActions position='left'>
<ButtonPrevious onClick={goBack}>
<Arrow direction='left' />
</ButtonPrevious>
</HeaderActions>
return (
<Wrapper padding='0 16px'>
<Header>
<ButtonPrevious aria-label='Back' onClick={handleBackClick}>
<Arrow direction='left' />
</ButtonPrevious>
<ChooseSub>
<T9n t={titleLexic} />
</ChooseSub>
</Header>
<Body>
<Packages
onButtonClick={onHandleClick}
buttonId={buttonId}
buttonLexic={buttonLexic}
/>
</Body>
{selectedPackage && isIframePayment && (
<IframePayment
match={match}
open={isOpenIframe}
paymentSystem={paymentSystem}
selectedPackage={selectedPackage}
setIsOpenIframe={setIsOpenIframe}
/>
)}
<HeaderTitle>
{hasPreviousStep && selectedSubscription ? (
<T9n t={selectedSubscription?.lexic} />
) : (
<Fragment>
<Name nameObj={match.team1} />
{` ${MDASH} `}
<Name nameObj={match.team2} />
</Fragment>
)}
</HeaderTitle>
</Wrapper>
)
}
return (
<Wrapper
width={hasOnlyOneSubscription ? 624 : undefined}
padding={hasOnlyOneSubscription ? '60px' : '60px 80px'}
>
<Header>
<ChooseSub>
<T9n t={titleLexic} />
</ChooseSub>
<HeaderActions position='right'>
<CloseButton onClick={close} />
</HeaderActions>
</Header>
<Body marginTop={20}>
<Body marginTop={40}>
<Packages />
{!isIframePayment && <SelectedCard />}
</Body>
<Footer>
<Footer hasCard={hasCard}>
{!isIframePayment && <SelectedCard />}
<Button
disabled={!selectedPackage || disabledBuyBtn}
onClick={(e) => {
if (user) {
onHandleClick(e)
} else {
setSearch(window.location.search)
logout('saveToken')
}
}}
id='purchase_buy'
onClick={onHandleClick}
id={buttonId}
>
{loader ? (
<ArrowLoader disabled />
) : (
<T9n t='buy_subscription' />
<T9n t={buttonLexic} />
)}
</Button>
</Footer>

@ -0,0 +1,22 @@
import styled from 'styled-components/macro'
import { isMobileDevice } from 'config'
import { Footer as FooterBase } from 'features/BuyMatchPopup/styled'
export const ChooseSub = styled.div`
font-weight: 700;
font-size: ${isMobileDevice ? 16 : 24}px;
margin: auto;
color: ${({ theme }) => theme.colors.white};
`
type FooterProps = {
hasCard?: boolean,
}
export const Footer = styled(FooterBase)<FooterProps>`
flex-direction: column;
align-items: center;
margin-top: ${({ hasCard }) => (hasCard ? 20 : 40)}px;
`

@ -1,41 +1,181 @@
import styled from 'styled-components/macro'
import type { MouseEvent } from 'react'
import { useMemo } from 'react'
import { PaymentPeriodTabs } from 'features/PaymentPeriodTabs'
import reduce from 'lodash/reduce'
import sortBy from 'lodash/sortBy'
import without from 'lodash/without'
import { isMobileDevice } from 'config'
import type { MatchPackage } from 'features/BuyMatchPopup/types'
import { useLexicsConfig } from 'features/LexicsStore'
import { Carousel, type Breakpoints } from 'components/Carousel'
import { useBuyMatchPopupStore } from '../../store'
import { PackagesList } from '../PackagesList'
const Wrapper = styled.div`
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
`
export const Packages = () => {
const {
onPackageSelect,
onPeriodSelect,
selectedPackage,
selectedPeriod,
selectedSubscription,
subscriptions,
} = useBuyMatchPopupStore()
if (!selectedSubscription) return null
import { RegularPackage } from '../RegularPackage'
import { PackageMobile } from '../PackageMobile'
import {
Wrapper,
List,
PrevButton,
NextButton,
Arrow,
ListItem,
} from './styled'
import { SinglePackage } from '../SinglePackage'
const breakpoints: Breakpoints = {
1000: { items: 3 },
1280: { items: 4 },
1400: { items: 5 },
}
type ButtonProps = {
disabled?: boolean,
onClick: () => void,
}
const renderPrevButton = ({ disabled, onClick }: ButtonProps) => (
<PrevButton
aria-label='Previous'
disabled={disabled}
onClick={onClick}
>
<Arrow direction='left' />
</PrevButton>
)
const renderNextButton = ({ disabled, onClick }: ButtonProps) => (
<NextButton
aria-label='Next'
disabled={disabled}
onClick={onClick}
>
<Arrow direction='right' />
</NextButton>
)
type Props = {
buttonId?: string,
buttonLexic?: string,
onButtonClick?: (e: MouseEvent<HTMLButtonElement>, matchPackage?: MatchPackage) => void,
}
export const Packages = ({
buttonId,
buttonLexic,
onButtonClick,
}: Props) => {
const { matchPackages } = useBuyMatchPopupStore()
const hasOnlyOneSubscription = matchPackages.length === 1
const hasMoreThanFiveSubscriptions = matchPackages.length > 5
const getSortedPackages = () => {
let temp = sortBy(matchPackages, 'order')
const mainPackage = matchPackages.find(({ isMainPackage }) => isMainPackage)
if (mainPackage && matchPackages.length > 1) {
const index = matchPackages.length >= 2 && matchPackages.length < 4 ? 1 : 2
temp = without(temp, mainPackage)
temp.splice(
index,
0,
mainPackage,
)
}
return temp
}
const sortedPackages = getSortedPackages()
const lexicsIds = useMemo(
() => [...reduce<MatchPackage, Set<number>>(
matchPackages,
(acc, { originalObject: { sub } }) => {
acc.add(sub.lexic1)
sub.lexic2 && acc.add(sub.lexic2)
acc.add(sub.lexic3)
return acc
},
new Set(),
)],
[matchPackages],
)
useLexicsConfig(lexicsIds)
if (isMobileDevice) {
return (
<Wrapper>
<List>
{sortedPackages.map((matchPackage) => (
<ListItem key={matchPackage.id}>
<PackageMobile
matchPackage={matchPackage}
onButtonClick={onButtonClick}
buttonId={buttonId}
buttonLexic={buttonLexic}
/>
</ListItem>
))}
</List>
</Wrapper>
)
}
if (hasOnlyOneSubscription) {
return (
<SinglePackage matchPackage={matchPackages[0]} />
)
}
const getWrapperWidth = () => {
switch (true) {
case !hasMoreThanFiveSubscriptions:
return undefined
case window.innerWidth < 1500:
return window.innerWidth - 160
default:
return 1380
}
}
const wrapperWidth = getWrapperWidth()
return (
<Wrapper>
<PaymentPeriodTabs
onPeriodSelect={onPeriodSelect}
selectedPeriod={selectedPeriod}
selectedSubscription={selectedSubscription}
/>
<PackagesList
packages={subscriptions}
selectedPackage={selectedPackage}
onSelect={onPackageSelect}
/>
<Wrapper width={wrapperWidth}>
{hasMoreThanFiveSubscriptions
? (
<Carousel
animationDuration={400}
infinite
useKeyboardNavigation
breakpoints={breakpoints}
spaceBetween={20}
renderPrevButton={renderPrevButton}
renderNextButton={renderNextButton}
>
{sortedPackages.map((matchPackage) => (
<RegularPackage matchPackage={matchPackage} />
))}
</Carousel>
)
: (
<List>
{sortedPackages.map((matchPackage) => (
<ListItem key={matchPackage.id}>
<RegularPackage matchPackage={matchPackage} />
</ListItem>
))}
</List>
)}
</Wrapper>
)
}

@ -0,0 +1,65 @@
import styled, { css } from 'styled-components/macro'
import { isMobileDevice } from 'config'
import { ArrowButton, Arrow as ArrowBase } from 'features/HeaderFilters/components/DateFilter/styled'
type WrapperProps = {
width?: number,
}
export const Wrapper = styled.div<WrapperProps>`
width: ${({ width }) => (width ? `${width}px` : 'auto')};
margin: auto;
`
export const List = styled.ul`
display: flex;
gap: 20px;
${isMobileDevice
? css`
display: initial;
max-height: calc(100vh - 140px);
overflow-y: auto;
`
: ''}
`
export const ListItem = styled.li`
width: 260px;
${isMobileDevice
? css`
width: 100%;
margin-bottom: 12px;
border-radius: 2px;
overflow: hidden;
`
: ''}
`
const NavButton = styled(ArrowButton)`
position: absolute;
top: 50%;
translate: 0 -50%;
${({ disabled }) => (disabled
? css`
opacity: 0.5;
`
: '')}
`
export const PrevButton = styled(NavButton)`
left: 30px;
`
export const NextButton = styled(NavButton)`
right: 30px;
`
export const Arrow = styled(ArrowBase)`
width: 20px;
height: 20px;
`

@ -1,84 +0,0 @@
import isNumber from 'lodash/isNumber'
import map from 'lodash/map'
import { T9n } from 'features/T9n'
import { MatchPackage, SubscriptionType } from 'features/BuyMatchPopup/types'
import {
Pass,
Name,
Description,
InfoWrapper,
Item,
List,
Price,
ScAutoRenewal,
ScPriceContainer,
} from './styled'
type Props = {
onSelect: (subscription: MatchPackage) => void,
packages: Array<MatchPackage>,
selectedPackage: MatchPackage | null,
}
export const PackagesList = ({
onSelect,
packages,
selectedPackage,
}: Props) => (
<List>
{
map(
packages,
(subPackage) => (
<Item
key={subPackage.id}
onClick={() => onSelect(subPackage)}
active={subPackage === selectedPackage}
>
<InfoWrapper>
<Pass>
<T9n t={subPackage.pass} />
</Pass>
<Name>
{
isNumber(subPackage.nameLexic)
? <T9n t={subPackage.nameLexic} />
: subPackage.name
}
</Name>
<Description>
<T9n
t={subPackage.description.lexic}
values={subPackage.description.values}
/>
</Description>
</InfoWrapper>
<ScPriceContainer>
<Price
amount={subPackage.price}
currency={subPackage.currency}
perPeriod={
subPackage.type !== SubscriptionType.Month
? null
: `per_${subPackage.type}`
}
/>
{
subPackage.type === SubscriptionType.Month
&& (
<ScAutoRenewal>
<T9n
t='auto_renewal'
/>
</ScAutoRenewal>
)
}
</ScPriceContainer>
</Item>
),
)
}
</List>
)

@ -1,230 +0,0 @@
import styled, { css } from 'styled-components/macro'
import { isMobileDevice } from 'config/userAgent'
import { popupScrollbarStyles } from 'features/PopupComponents'
import { Price as BasePrice } from 'features/Price'
import {
PriceAmount,
PriceDetails,
Period,
} from 'features/Price/styled'
export const List = styled.ul`
width: 100%;
height: 460px;
overflow-y: auto;
margin-top: 25px;
padding: 0 40px;
${popupScrollbarStyles}
@media (max-width: 1370px) {
max-height: 415px;
height: auto;
}
@media (max-height: 768px) {
max-height: 280px;
}
@media (max-height: 500px) {
max-height: 140px;
}
${isMobileDevice
? css`
padding: 0;
margin-top: 19px;
@media screen and (orientation: landscape){
margin-top: 10px;
}
`
: ''};
`
type ItemProps = {
active?: boolean,
}
export const Item = styled.li.attrs(() => ({
tabIndex: 0,
}))<ItemProps>`
width: 100%;
min-height: 140px;
padding: 20px 30px 20px 20px;
background: ${({ theme }) => theme.colors.packageBackground};
box-shadow: 0px 1px 1px rgba(0, 0, 0, 0.3);
border-radius: 2px;
display: flex;
justify-content: space-between;
align-items: center;
cursor: pointer;
transition: background-color 0.3s;
:not(:last-child) {
margin-bottom: 20px;
}
${({ active, theme: { colors } }) => (
active ? `background-color: ${colors.button}` : ''
)};
@media (max-width: 1370px) {
min-height: 125px;
}
@media (max-width: 750px) {
height: 100%;
}
${isMobileDevice
? css`
padding: 5px 10px;
@media (max-width: 750px) {
min-height: 52.29px;
height: auto;
}
:not(:last-child) {
margin-bottom: 10px;
@media screen and (orientation: landscape) {
margin-bottom: 5.24px;
}
}
` : ''};
@media (max-width: 850px) and (orientation: landscape){
min-height: 52.29px;
height: auto;
}
`
export const InfoWrapper = styled.div`
width: 75%;
display: flex;
flex-direction: column;
align-self: flex-start;
${isMobileDevice
? css`
align-self: center;
@media (max-width: 850px) and (orientation: landscape){
height: 100%;
}
`
: ''};
`
export const Name = styled.span`
font-weight: 500;
font-size: 20px;
line-height: 23px;
letter-spacing: 0.03em;
@media (max-width: 1370px) {
line-height: 20px;
}
${isMobileDevice
? css`
@media (max-width: 750px){
font-size: 12px;
line-height: 10.04px;
}
@media screen and (orientation: landscape){
font-size: 14px;
}
`
: ''};
`
export const Pass = styled(Name)`
font-weight: 600;
text-transform: uppercase;
${isMobileDevice
? css`
line-height: 12px;
@media screen and (orientation: landscape){
line-height: 14px;
}
`
: ''};
`
export const Description = styled.span`
width: 68%;
margin-top: 13px;
font-weight: 500;
font-size: 15px;
line-height: 20px;
letter-spacing: 0.03em;
@media (max-width: 1370px) {
line-height: 18px;
}
${isMobileDevice
? css`
@media (max-width: 750px){
font-size: 8px;
line-height: 8px;
margin-top: 5px;
width: 100%;
}
@media (max-width: 850px) and (orientation: landscape){
margin-top: 0;
line-height: 8px;
font-size: 10px;
}
`
: ''};
`
export const Price = styled(BasePrice)`
${PriceAmount} {
font-size: 24px;
line-height: 24px;
font-weight: normal;
${isMobileDevice
? css`
font-size: 14px;
`
: ''};
}
${PriceDetails} {
font-weight: 500;
font-size: 12px;
line-height: 18px;
${isMobileDevice
? css`
font-size: 8px;
`
: ''};
}
${Period} {
text-transform: capitalize;
}
`
export const ScAutoRenewal = styled(PriceDetails)`
line-height: 21px;
font-size: 12px;
text-transform: none;
margin: 0;
color: ${({ theme: { colors } }) => colors.white70};
${isMobileDevice
? css`
line-height: normal;
font-size: 10px;
text-align: center;
`
: ''};
`
export const ScPriceContainer = styled.div`
display: flex;
flex-direction: column;
`

@ -0,0 +1,74 @@
import type { MatchPackage } from 'features/BuyMatchPopup/types'
import { SubscriptionType } from 'features/BuyMatchPopup/types'
import { T9n } from 'features/T9n'
import { Price } from 'features/Price'
import { usePackage } from './usePackage'
import {
Wrapper,
InfoWrapper,
Description,
Header,
HeaderTitle,
BestChoice,
PriceBlock,
PriceTextWrapper,
PriceTextTop,
PriceTextBottom,
} from './styled'
type Props = {
matchPackage: MatchPackage,
}
export const RegularPackage = ({ matchPackage }: Props) => {
const {
firstDescription,
handleClick,
handleKeyPress,
isActive,
priceTextBottomLexic,
priceTextTopLexic,
} = usePackage(matchPackage)
return (
<Wrapper
onClick={handleClick}
onKeyDown={handleKeyPress}
active={isActive}
isMainPackage={matchPackage.isMainPackage}
>
<Header>
<HeaderTitle
width={matchPackage.id.startsWith('team_') ? 190 : undefined}
t={matchPackage.originalObject.sub.lexic1}
/>
{matchPackage.isMainPackage && (
<BestChoice>
<T9n t='best_choice' />
</BestChoice>
)}
</Header>
<InfoWrapper>
<Description>
{firstDescription}
</Description>
<Description>
<T9n t={matchPackage.originalObject.sub.lexic3} />
</Description>
<PriceBlock isMonthSubscription={matchPackage.isMonthSubscription}>
<PriceTextWrapper>
<PriceTextTop t={priceTextTopLexic} />
<PriceTextBottom t={priceTextBottomLexic} />
</PriceTextWrapper>
<Price
amount={matchPackage.originalObject.price}
currency={matchPackage.currency}
perPeriod={matchPackage.isMonthSubscription ? `per_${SubscriptionType.Month}` : undefined}
/>
</PriceBlock>
</InfoWrapper>
</Wrapper>
)
}

@ -0,0 +1,140 @@
import styled, { css } from 'styled-components/macro'
import { PriceDetails } from 'features/Price/styled'
import { T9n } from 'features/T9n'
export const Description = styled.p`
margin-bottom: 20px;
font-weight: 400;
font-size: 12px;
line-height: 20px;
white-space: initial;
:empty, :has(span:empty) {
display: none;
}
:last-of-type {
margin-bottom: 0;
}
`
type PriceBlockProps = {
isMonthSubscription?: boolean,
}
export const PriceBlock = styled.div<PriceBlockProps>`
display: flex;
justify-content: space-between;
align-items: center;
position: absolute;
top: 218px;
left: 0;
right: 0;
padding-left: 30px;
padding-right: ${({ isMonthSubscription }) => (isMonthSubscription ? 10 : 20)}px;
`
export const PriceTextWrapper = styled.div`
display: flex;
flex-direction: column;
white-space: initial;
`
export const PriceTextTop = styled(T9n)`
margin-bottom: 2px;
font-weight: 600;
font-size: 10px;
font-style: italic;
letter-spacing: 0.03em;
`
export const PriceTextBottom = styled(T9n)`
font-weight: 400;
font-size: 8px;
font-style: italic;
letter-spacing: 0.03em;
`
type WrapperProps = {
active?: boolean,
isMainPackage?: boolean,
}
export const Wrapper = styled.div.attrs({
tabIndex: 0,
})<WrapperProps>`
width: 100%;
height: 380px;
border-radius: 2px;
background: ${({ theme }) => theme.colors.packageBackground};
box-shadow: 0px 0px 10px 5px rgba(0, 0, 0, 0.1);
transition: background-color 0.3s;
cursor: pointer;
${({ active, theme: { colors } }) => (active
? css`
background-color: ${colors.button};
`
: css`
:hover {
background-color: #2F3E6D;
}`
)}
${({ isMainPackage, theme: { colors } }) => (isMainPackage
? css`
border: 2px solid ${colors.white};
`
: ''
)}
`
export const Header = styled.header`
display: flex;
flex-direction: column;
justify-content: center;
height: 100px;
background: rgba(255, 255, 255, 0.2);
`
type HeaderTitleProps = {
width?: number,
}
export const HeaderTitle = styled(T9n)<HeaderTitleProps>`
flex: 1;
display: flex;
align-items: center;
width: ${({ width }) => (width ? `${width}px` : 'auto')};
padding-left: 30px;
white-space: initial;
font-size: 16px;
font-weight: 600;
`
export const BestChoice = styled.div`
display: flex;
align-items: center;
height: 22px;
margin-top: auto;
padding-left: 32px;
font-size: 12px;
font-weight: 600;
letter-spacing: 0.03em;
font-variant: all-small-caps;
color: #464646;
background-color: #FDFDFE;
`
export const InfoWrapper = styled.div`
position: relative;
display: flex;
flex-direction: column;
height: calc(100% - 100px);
padding: 20px 30px 30px;
${PriceDetails} {
margin-top: 5px;
}
`

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

Loading…
Cancel
Save