Compare commits

...

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

@ -1007,3 +1007,56 @@ steps:
- 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

@ -225,6 +225,15 @@ rustat-prod:
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:

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.

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>

Before

Width:  |  Height:  |  Size: 2.8 KiB

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>

@ -155,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,
@ -170,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,
@ -186,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,
@ -199,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,
@ -212,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,

@ -44,6 +44,12 @@ 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]
@ -55,3 +61,4 @@ 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,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>

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

@ -24,6 +24,10 @@ import {
setCookie,
removeCookie,
isMatchPage,
REFRESH_TOKEN_KEY,
removeRefreshToken,
writeRefreshToken,
readRefreshToken,
} from 'helpers'
import {
@ -77,6 +81,7 @@ export const useAuth = () => {
userManager.signoutRedirect({ post_logout_redirect_uri: urlWithLang })
})
removeToken()
removeRefreshToken()
if (key !== 'saveToken') {
removeCookie('access_token')
}
@ -158,12 +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)
@ -175,7 +183,7 @@ export const useAuth = () => {
setPage('')
setSearch('')
}).catch(login)
// eslint-disable-next-line react-hooks/exhaustive-deps
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
login,
storeUser,
@ -187,11 +195,14 @@ export const useAuth = () => {
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() : checkUser()
// eslint-disable-next-line react-hooks/exhaustive-deps
isRedirectedBackFromAuthProvider
? signinRedirectCallback(refreshToken)
: checkUser()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
checkUser,
signinRedirectCallback,
@ -221,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)
@ -231,6 +242,7 @@ export const useAuth = () => {
checkNewDevice,
isNewDeviceLogin,
setIsNewDeviceLogin,
user,
])
duel.channel('active_page') // поле в LS, определяющее активность вкладки
@ -240,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

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

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

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

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

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

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

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

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

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

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

@ -74,6 +74,7 @@ export const HeaderStyled = styled.header<HeaderProps>`
? css`
padding: 8px;
margin-bottom: 50px;
justify-content: flex-start;
`
: ''}
`

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -9,8 +9,8 @@ import { SELECTED_API_KEY } from 'helpers/selectedApi'
import { useToggle } from 'hooks/useToggle'
import { useLocalStore } from 'hooks/useStorage'
import { removeToken } from '../../helpers'
import { removeCookie } from '../../helpers/cookie'
import { removeRefreshToken, removeToken } from 'helpers'
import { removeCookie } from 'helpers/cookie'
type FormElement = HTMLFormElement & {
api: HTMLInputElement & {
@ -37,6 +37,7 @@ export const useSystemSettings = () => {
const { api } = e.currentTarget
setSelectedApi(api.value)
removeToken()
removeRefreshToken()
removeCookie('access_token')
window.location.reload()
}

@ -10,9 +10,10 @@ import {
import { AdType } from 'requests'
import type { Match } from 'helpers'
import { T9n } from 'features/T9n'
import { Icon } from 'features/Icon'
import type { Match } from 'features/Matches'
import { MatchCard } from 'features/MatchCard'
import {
CountryFlag,

@ -4,10 +4,11 @@ import orderBy from 'lodash/orderBy'
import { ProfileTypes } from 'config'
import { TournamentListProps } from 'features/TournamentList'
import type { Match } from 'features/Matches'
import { useHeaderFiltersStore } from 'features/HeaderFilters'
import { useUserFavoritesStore } from 'features/UserFavorites/store'
import type { Match } from 'helpers'
interface TournamentsSortProps {
id: number,
isFavorite: boolean,
@ -45,7 +46,7 @@ export const useTournaments = (matches: Array<Match>) => {
match.tournament.is_super_tournament ? match.group.id : match.tournament.id,
)
if (!acc[`${match.sportType}_${match.tournament.id}`] && compareSport(match, selectedSport)
if (!acc[`${match.sportType}_${match.tournament.id}`] && compareSport(match.sportType, selectedSport)
&& compareLeague(uniqTournamentId)) {
const tournament = {
...match.tournament,
@ -68,7 +69,7 @@ export const useTournaments = (matches: Array<Match>) => {
isSuperTournament: Boolean(match.tournament.is_super_tournament),
sportType: match.sportType,
})
} else if (compareSport(match, selectedSport) && compareLeague(uniqTournamentId)) {
} else if (compareSport(match.sportType, selectedSport) && compareLeague(uniqTournamentId)) {
acc[uniqTournamentId] = {
...acc[uniqTournamentId],
tournament: {

@ -1,6 +1,7 @@
import { useRouteMatch } from 'react-router-dom'
import type { Match } from 'features/Matches'
import type { Match } from 'helpers'
import { MatchCard } from 'features/MatchCard'
import type { TournamentType } from 'requests/getMatches/types'

@ -14,7 +14,6 @@ export const UserSportFavWrapper = styled.aside`
flex-direction: column;
align-items: center;
margin-top: 1rem;
padding-bottom: 2.75rem;
left: 0;
top: 0;
bottom: 0;
@ -29,7 +28,7 @@ export const UserSportFavWrapper = styled.aside`
export const ScrollWrapper = styled.div`
width: 4.35rem;
padding: 0.472rem 0.30rem 0 0.15rem;
padding: 0.472rem 0.30rem 0.472rem 0.15rem;
height: calc(100vh - 14rem);
display: flex;
flex-direction: column;

@ -17,3 +17,4 @@ export * from './languageUrlParam'
export * from './bodyScrollLock'
export * from './getLocalStorage'
export * from './checkPage'
export * from './prepareMatches'

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

@ -1,4 +1,5 @@
export const TOKEN_KEY = 'token'
export const REFRESH_TOKEN_KEY = 'refresh_token'
export const readToken = () => (
localStorage.getItem(TOKEN_KEY)
@ -11,3 +12,15 @@ export const writeToken = (token: string) => (
export const removeToken = () => (
localStorage.removeItem(TOKEN_KEY)
)
export const removeRefreshToken = () => {
localStorage.removeItem(REFRESH_TOKEN_KEY)
}
export const writeRefreshToken = (token: string) => (
localStorage.setItem(REFRESH_TOKEN_KEY, token)
)
export const readRefreshToken = () => (
localStorage.getItem(REFRESH_TOKEN_KEY)
)

@ -10,3 +10,5 @@ export * from './useModalRoot'
export * from './usePageLogger'
export * from './useDuration'
export * from './useScreenOrientation'
export * from './useWebSocket'
export * from './useVideoBounds'

@ -0,0 +1,19 @@
import { useQueryClient } from 'react-query'
import { querieKeys } from 'config'
import type { MatchScore } from 'requests'
import { useMatchPageStore } from 'features/MatchPage/store'
export const useVideoBounds = () => {
const { profile: matchProfile } = useMatchPageStore()
const client = useQueryClient()
const matchScore = client.getQueryData<MatchScore>(querieKeys.matchScore)
const videoBounds = matchScore?.video_bounds || matchProfile?.video_bounds
return videoBounds
}

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

Loading…
Cancel
Save