parent
0e0f24454e
commit
d06425df94
|
After Width: | Height: | Size: 2.8 KiB |
@ -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, |
||||||
|
} |
||||||
|
} |
||||||
@ -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}'</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> |
||||||
|
) |
||||||
|
} |
||||||
@ -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}; |
||||||
|
} |
||||||
|
} |
||||||
|
` |
||||||
@ -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}; |
||||||
|
} |
||||||
|
} |
||||||
|
` |
||||||
@ -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, |
||||||
|
} |
||||||
|
} |
||||||
@ -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 |
||||||
|
} |
||||||
@ -0,0 +1,128 @@ |
|||||||
|
import { |
||||||
|
useEffect, |
||||||
|
useRef, |
||||||
|
useState, |
||||||
|
} from 'react' |
||||||
|
|
||||||
|
interface WebSocketHandlers<T> { |
||||||
|
onClose?: (event: CloseEvent) => void, |
||||||
|
onError?: (event: Event) => void, |
||||||
|
onMessage?: (data: T) => void, |
||||||
|
onOpen?: (event: Event) => void, |
||||||
|
} |
||||||
|
|
||||||
|
interface WebSocketOptions<T> { |
||||||
|
allowConnection?: boolean, // Разрешено ли подключение
|
||||||
|
autoReconnect?: boolean, // Автоматическое переподключение при ошибке
|
||||||
|
handlers?: WebSocketHandlers<T>, // Обработчики событий
|
||||||
|
maxReconnectAttempts?: number, // Максимальное количество попыток переподключения
|
||||||
|
reconnectInterval?: number, // Интервал между попытками переподключения
|
||||||
|
url: string, // Url запроса
|
||||||
|
} |
||||||
|
|
||||||
|
export const useWebSocket = <T, >({ |
||||||
|
allowConnection = true, |
||||||
|
autoReconnect = false, |
||||||
|
handlers = {}, |
||||||
|
maxReconnectAttempts = 5, |
||||||
|
reconnectInterval = 5000, |
||||||
|
url, |
||||||
|
}: WebSocketOptions<T>) => { |
||||||
|
const { |
||||||
|
onClose, |
||||||
|
onError, |
||||||
|
onMessage, |
||||||
|
onOpen, |
||||||
|
} = handlers |
||||||
|
|
||||||
|
const webSocketRef = useRef<WebSocket | null>(null) |
||||||
|
const timeoutIdRef = useRef<NodeJS.Timeout | null>(null) |
||||||
|
const reconnectAttemptsRef = useRef<number>(0) |
||||||
|
const onCloseRef = useRef(onClose) |
||||||
|
const onErrorRef = useRef(onError) |
||||||
|
const onMessageRef = useRef(onMessage) |
||||||
|
const onOpenRef = useRef(onOpen) |
||||||
|
const [webSocketState, setWebSocketState] = useState(WebSocket.CONNECTING) |
||||||
|
|
||||||
|
const sendMessage = (message: Record<string, unknown>) => { |
||||||
|
if (webSocketRef.current?.readyState !== WebSocket.OPEN) return |
||||||
|
|
||||||
|
const body = JSON.stringify(message) |
||||||
|
|
||||||
|
webSocketRef.current?.send(body) |
||||||
|
} |
||||||
|
|
||||||
|
const closeConnection = () => { |
||||||
|
setWebSocketState(WebSocket.CLOSING) |
||||||
|
webSocketRef.current?.close() |
||||||
|
} |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
onCloseRef.current = onClose |
||||||
|
onErrorRef.current = onError |
||||||
|
onMessageRef.current = onMessage |
||||||
|
onOpenRef.current = onOpen |
||||||
|
}, [onClose, onError, onMessage, onOpen]) |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
const connect = () => { |
||||||
|
if (!allowConnection) return |
||||||
|
|
||||||
|
const ws = new WebSocket(url) |
||||||
|
|
||||||
|
ws.addEventListener('open', handleOpen) |
||||||
|
ws.addEventListener('message', handleMessage) |
||||||
|
ws.addEventListener('close', handleClose) |
||||||
|
ws.addEventListener('error', handleError) |
||||||
|
|
||||||
|
webSocketRef.current = ws |
||||||
|
setWebSocketState(WebSocket.CONNECTING) |
||||||
|
} |
||||||
|
|
||||||
|
const handleOpen = (event: Event) => { |
||||||
|
setWebSocketState(WebSocket.OPEN) |
||||||
|
onOpenRef.current?.(event) |
||||||
|
reconnectAttemptsRef.current = 0 |
||||||
|
} |
||||||
|
|
||||||
|
const handleMessage = (event: MessageEvent) => { |
||||||
|
const data = JSON.parse(event.data) |
||||||
|
|
||||||
|
onMessageRef.current?.(data) |
||||||
|
} |
||||||
|
|
||||||
|
const handleClose = (event: CloseEvent) => { |
||||||
|
setWebSocketState(WebSocket.CLOSED) |
||||||
|
onCloseRef.current?.(event) |
||||||
|
} |
||||||
|
|
||||||
|
const handleError = (event: Event) => { |
||||||
|
setWebSocketState(WebSocket.CLOSED) |
||||||
|
onErrorRef.current?.(event) |
||||||
|
|
||||||
|
if (autoReconnect && reconnectAttemptsRef.current < maxReconnectAttempts) { |
||||||
|
timeoutIdRef.current = setTimeout(connect, reconnectInterval) |
||||||
|
reconnectAttemptsRef.current++ |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
connect() |
||||||
|
|
||||||
|
return () => { |
||||||
|
webSocketRef.current?.removeEventListener('open', handleOpen) |
||||||
|
webSocketRef.current?.removeEventListener('message', handleMessage) |
||||||
|
webSocketRef.current?.removeEventListener('close', handleClose) |
||||||
|
webSocketRef.current?.removeEventListener('error', handleError) |
||||||
|
webSocketRef.current?.close() |
||||||
|
|
||||||
|
timeoutIdRef.current && clearTimeout(timeoutIdRef.current) |
||||||
|
} |
||||||
|
}, [url, autoReconnect, reconnectInterval, maxReconnectAttempts, allowConnection]) |
||||||
|
|
||||||
|
return { |
||||||
|
closeConnection, |
||||||
|
sendMessage, |
||||||
|
webSocketState, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
Loading…
Reference in new issue