diff --git a/src/App.tsx b/src/App.tsx
index 8f334b3..75137f8 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,16 +1,10 @@
-import {
- Dispatch,
- SetStateAction,
- createContext,
- useEffect,
- useState,
-} from 'react'
+import { Dispatch, SetStateAction, createContext, useState } from 'react'
import Footer from './components/UI/footer/Footer'
import StreamFeed from './components/streamFeed/StreamFeed'
import Navigation from './components/UI/navigation/Navigation'
import { StreamProps } from './types/StreamProps'
-import { getEnglishLanguageName } from './helper/getEnglishLanguageName'
import { getItemFromStorage } from './helper/getItemFromStorage'
+import { useDocumentTitle } from './hooks/useDocumentTitle'
import { useScreenWidth } from './hooks/useScreenWidth'
export const ContextScreenWidth = createContext<
@@ -85,11 +79,7 @@ const App = () => {
const [inputFocussed, setInputFocussed] = useState(false)
const [hideSearch, setHideSearch] = useState(true)
- useEffect(() => {
- document.title = `Twitch-App | ${getEnglishLanguageName(
- language
- )} Livestreams${seoSearchText ? ` | ${seoSearchText}` : ''}`
- }, [language, seoSearchText])
+ useDocumentTitle(language, seoSearchText)
return (
diff --git a/src/components/UI/navigation/DesktopSearch.tsx b/src/components/UI/navigation/DesktopSearch.tsx
index 7a76293..1205119 100644
--- a/src/components/UI/navigation/DesktopSearch.tsx
+++ b/src/components/UI/navigation/DesktopSearch.tsx
@@ -1,14 +1,16 @@
-import { forwardRef, useContext, useEffect, useRef } from 'react'
+import { forwardRef, useContext, useRef } from 'react'
+import './search.css'
import {
ContextDisableFocusTrap,
ContextFocusInput,
ContextSearchResults,
ContextSearchText,
} from '../../../App'
-import { SearchProps } from '../../../types/SearchProps'
import Icon from '../Icon'
import SearchResultSuggestion from './SearchResultSuggestion'
-import './search.css'
+import { SearchProps } from '../../../types/SearchProps'
+import { useFocusInput } from '../../../hooks/useFocusInput'
+import { useFocusTrapSearch } from '../../../hooks/useFocusTrapSearch'
const DesktopSearch = forwardRef
(
(
@@ -66,68 +68,14 @@ const DesktopSearch = forwardRef(
const buttonRef = useRef(null)
const searchResultsRef = useRef(null)
- useEffect(() => {
- const handleFocusTrap = (e: KeyboardEvent) => {
- if (
- searchText.length === 0 ||
- focusTrapDisabled ||
- e.key !== 'Tab'
- )
- return
-
- const focusableElements = [
- inputRef?.current,
- buttonRef.current,
- ...(searchResultsRef.current
- ? Array.from(
- searchResultsRef.current.querySelectorAll(
- 'button'
- )
- )
- : []),
- ].filter((el) => el !== null) as (
- | HTMLInputElement
- | HTMLButtonElement
- )[]
-
- if (focusableElements.length === 0) return
-
- const firstElement = focusableElements[0]
- const lastElement =
- focusableElements[focusableElements.length - 1]
-
- if (e.shiftKey) {
- if (document.activeElement === firstElement) {
- e.preventDefault()
- lastElement.focus()
- }
- } else {
- if (document.activeElement === lastElement) {
- e.preventDefault()
- firstElement.focus()
- }
- }
- }
-
- document.addEventListener('keydown', handleFocusTrap)
-
- return () => {
- document.removeEventListener('keydown', handleFocusTrap)
- }
- }, [
+ useFocusTrapSearch(
+ buttonRef,
focusTrapDisabled,
inputRef,
- searchResults,
- searchResultsExpanded,
- searchText,
- ])
-
- useEffect(() => {
- if (inputFocussed && inputRef?.current) {
- inputRef.current.focus()
- setInputFocussed(false)
- }
- }, [inputFocussed, inputRef, setInputFocussed])
+ searchResultsRef,
+ searchText
+ )
+ useFocusInput(inputFocussed, inputRef, setInputFocussed)
return (
(
(
@@ -66,62 +68,14 @@ const MobileSearch = forwardRef
(
const buttonRef = useRef(null)
const searchResultsRef = useRef(null)
- useEffect(() => {
- const handleFocusTrap = (e: KeyboardEvent) => {
- if (e.key !== 'Tab' || focusTrapDisabled) return
-
- const focusableElements = [
- searchMobileRef?.current,
- buttonRef.current,
- ...(searchResultsRef.current
- ? Array.from(
- searchResultsRef.current.querySelectorAll(
- 'button'
- )
- )
- : []),
- ].filter((el) => el !== null) as (
- | HTMLInputElement
- | HTMLButtonElement
- )[]
-
- if (focusableElements.length === 0) return
-
- const firstElement = focusableElements[0]
- const lastElement =
- focusableElements[focusableElements.length - 1]
-
- if (e.shiftKey) {
- if (document.activeElement === firstElement) {
- e.preventDefault()
- lastElement.focus()
- }
- } else {
- if (document.activeElement === lastElement) {
- e.preventDefault()
- firstElement.focus()
- }
- }
- }
-
- document.addEventListener('keydown', handleFocusTrap)
-
- return () => {
- document.removeEventListener('keydown', handleFocusTrap)
- }
- }, [
+ useFocusTrapSearch(
+ buttonRef,
focusTrapDisabled,
searchMobileRef,
- searchResults,
- searchResultsExpanded,
- ])
-
- useEffect(() => {
- if (inputFocussed && searchMobileRef?.current) {
- searchMobileRef.current.focus()
- setInputFocussed(false)
- }
- }, [inputFocussed, searchMobileRef, setInputFocussed])
+ searchResultsRef,
+ searchText
+ )
+ useFocusInput(inputFocussed, searchMobileRef, setInputFocussed)
return (
<>
diff --git a/src/components/UI/navigation/Navigation.tsx b/src/components/UI/navigation/Navigation.tsx
index 56e2b3f..f18cc80 100644
--- a/src/components/UI/navigation/Navigation.tsx
+++ b/src/components/UI/navigation/Navigation.tsx
@@ -1,8 +1,9 @@
-import { useContext, useEffect, useRef, useState } from 'react'
+import { useContext, useRef, useState } from 'react'
import ButtonIcon from '../ButtonIcon'
+import DesktopSearch from './DesktopSearch'
+import MobileSearch from './MobileSearch'
import { HomeIcon } from './HomeIcon'
import { UserIcon } from './UserIcon'
-import MobileSearch from './MobileSearch'
import {
ContextDisableFocusTrap,
ContextFilteredStreamData,
@@ -14,9 +15,11 @@ import {
ContextSearchText,
ContextStreamData,
} from '../../../App'
-import DesktopSearch from './DesktopSearch'
import { getSearchFilter } from '../../../helper/getSearchFilter'
import { setItemInStorage } from '../../../helper/setItemInStorage'
+import { useNavigationScrollY } from '../../../hooks/useNavigationScrollY'
+import { useCloseSearchResults } from '../../../hooks/useCloseSearchResults'
+import { useHideMobileSearch } from '../../../hooks/useHideMobileSearch'
const Navigation = () => {
const contextScreenWidth = useContext(ContextScreenWidth)
@@ -253,103 +256,24 @@ const Navigation = () => {
}, 0)
}
- useEffect(() => {
- let lastScrollY = window.scrollY
- let timer: NodeJS.Timeout
-
- const handleScroll = () => {
- if (window.scrollY === 0) {
- setNavOpacity('opacity-100')
- } else if (window.scrollY < lastScrollY && !blockOpacity) {
- setNavOpacity('opacity-75')
- } else if (!blockOpacity) {
- setNavOpacity('opacity-95')
- }
- lastScrollY = window.scrollY
-
- clearTimeout(timer)
- if (navOpacity !== 'opacity-100') {
- timer = setTimeout(() => {
- setNavOpacity('opacity-100')
- }, 500)
- }
- }
-
- window.addEventListener('scroll', handleScroll)
-
- return () => {
- window.removeEventListener('scroll', handleScroll)
- clearTimeout(timer)
- }
- }, [blockOpacity, navOpacity])
-
- useEffect(() => {
- const handleClickOutside = (event: MouseEvent) => {
- if (
- ((desktopSearchRef.current &&
- !desktopSearchRef.current.contains(event.target as Node)) ||
- (mobileSearchRef.current &&
- !mobileSearchRef.current.contains(
- event.target as Node
- ))) &&
- searchResultsExpanded
- ) {
- event.stopPropagation()
- setSearchResultsExpanded(false)
- }
- }
-
- const handleEscape = (event: KeyboardEvent) => {
- if (
- ((desktopSearchRef.current &&
- desktopSearchRef.current.contains(event.target as Node)) ||
- (mobileSearchRef.current &&
- mobileSearchRef.current.contains(
- event.target as Node
- ))) &&
- searchResultsExpanded &&
- event.key === 'Escape'
- ) {
- event.stopPropagation()
- setSearchResultsExpanded(false)
- if (!inputRef?.current?.onfocus) {
- if (
- contextScreenWidth === 'MOBILE' ||
- contextScreenWidth === 'TABLET_SMALL'
- ) {
- buttonIconRef?.current?.focus()
- } else {
- userIconRef?.current?.focus()
- anchorRef?.current?.focus()
- }
- }
- }
- }
-
- if (searchResultsExpanded) {
- document.addEventListener('keydown', handleEscape)
- document.addEventListener('mousedown', handleClickOutside)
- } else {
- document.removeEventListener('keydown', handleEscape)
- document.removeEventListener('mousedown', handleClickOutside)
- }
-
- return () => {
- document.removeEventListener('keydown', handleEscape)
- document.removeEventListener('mousedown', handleClickOutside)
- }
- }, [contextScreenWidth, searchResultsExpanded])
-
- useEffect(() => {
- if (
- (contextScreenWidth === 'MOBILE' ||
- contextScreenWidth === 'TABLET_SMALL') &&
- inputFocussed
- ) {
- setHideSearch(false)
- setAriaPressed(true)
- }
- }, [contextScreenWidth, inputFocussed, setHideSearch])
+ useNavigationScrollY(blockOpacity, navOpacity, setNavOpacity)
+ useCloseSearchResults(
+ anchorRef,
+ buttonIconRef,
+ contextScreenWidth,
+ desktopSearchRef,
+ inputRef,
+ mobileSearchRef,
+ searchResultsExpanded,
+ setSearchResultsExpanded,
+ userIconRef
+ )
+ useHideMobileSearch(
+ contextScreenWidth,
+ inputFocussed,
+ setAriaPressed,
+ setHideSearch
+ )
return (
diff --git a/src/components/UI/navigation/SettingsPopup.tsx b/src/components/UI/navigation/SettingsPopup.tsx
index dec679f..be53217 100644
--- a/src/components/UI/navigation/SettingsPopup.tsx
+++ b/src/components/UI/navigation/SettingsPopup.tsx
@@ -1,4 +1,4 @@
-import { FC, useContext, useEffect, useRef, useState } from 'react'
+import { FC, useContext, useRef, useState } from 'react'
import {
ContextLanguage,
ContextSEOSearchText,
@@ -12,6 +12,8 @@ import Icon from '../Icon'
import ProfilePicture from './ProfilePicture'
import { getLanguageIndex } from '../../../helper/getLanguageIndex'
import { setItemInStorage } from '../../../helper/setItemInStorage'
+import { useCloseSettingsPopup } from '../../../hooks/useCloseSettingsPopup'
+import { useSettingsPopupFocusTrap } from '../../../hooks/useSettingsPopupFocusTrap'
type SettingsPopupProps = {
handleButtonKeyDown: (e: React.KeyboardEvent
) => void
@@ -176,61 +178,12 @@ const SettingsPopup: FC = ({
window.location.reload()
}
- useEffect(() => {
- const handleEscape = (event: KeyboardEvent) => {
- if (
- popupLanguageRef.current &&
- !popupLanguageRef.current.contains(event.target as Node) &&
- filterLanguageExpanded &&
- event.key === 'Escape'
- ) {
- event.stopPropagation()
- setFilterLanguageExpanded(false)
- }
- }
-
- if (filterLanguageExpanded) {
- document.addEventListener('keydown', handleEscape)
- } else {
- document.removeEventListener('keydown', handleEscape)
- }
-
- return () => {
- document.removeEventListener('keydown', handleEscape)
- }
- }, [filterLanguageExpanded, setFilterLanguageExpanded])
-
- useEffect(() => {
- if (filterLanguageExpanded) return
-
- const buttons = popupRef.current?.querySelectorAll('button')
- if (!buttons || buttons.length === 0) return
-
- const firstButton = buttons[0]
- const lastButton = buttons[buttons.length - 1]
-
- const handleFocusTrap = (e: KeyboardEvent) => {
- if (e.key !== 'Tab') return
-
- if (e.shiftKey) {
- if (document.activeElement === firstButton) {
- e.preventDefault()
- lastButton.focus()
- }
- } else {
- if (document.activeElement === lastButton) {
- e.preventDefault()
- firstButton.focus()
- }
- }
- }
-
- document.addEventListener('keydown', handleFocusTrap)
-
- return () => {
- document.removeEventListener('keydown', handleFocusTrap)
- }
- }, [filterLanguageExpanded, popupRef])
+ useCloseSettingsPopup(
+ filterLanguageExpanded,
+ popupLanguageRef,
+ setFilterLanguageExpanded
+ )
+ useSettingsPopupFocusTrap(filterLanguageExpanded, popupRef)
return (
>] | undefined
@@ -61,20 +64,7 @@ export const UserIcon: FC
= ({ anchorRef, buttonRef }) => {
sessionStorage.setItem('twitch_logged_in', 'true')
}
- useEffect(() => {
- const isLoggedIn = sessionStorage.getItem('twitch_logged_in')
- isLoggedIn === 'true' && !user && fetchUser()
- const timer = setTimeout(
- () =>
- isLoggedIn === 'false' &&
- (sessionStorage.removeItem('twitch_logged_in'),
- sessionStorage.removeItem('twitch_access_state'),
- sessionStorage.removeItem('twitch_access_token'),
- sessionStorage.removeItem('twitch_user')),
- 0
- )
- return () => clearTimeout(timer)
- }, [user])
+ useLogUserOut(fetchUser, user)
const handleButtonClick = () => {
setDropdownActive((prev) => !prev)
@@ -98,74 +88,17 @@ export const UserIcon: FC = ({ anchorRef, buttonRef }) => {
}
}
- useEffect(() => {
- const getAuthUrl = async () => {
- if (state.length) {
- try {
- const response = await fetch(
- `https://twitch-backend.vercel.app/api/auth-url?state=${state}`
- )
- const data = await response.json()
- setRedirectUrl(data.url)
- } catch (error) {
- console.error('Error fetching auth URL:', error)
- }
- }
- }
- getAuthUrl()
- }, [state])
-
- useEffect(() => {
- if (redirectUrl) {
- window.location.href = redirectUrl
- }
- }, [redirectUrl])
-
- useEffect(() => {
- const handleClickOutside = (event: MouseEvent) => {
- if (
- popupRef.current &&
- !popupRef.current.contains(event.target as Node) &&
- buttonRef.current &&
- !buttonRef.current.contains(event.target as Node)
- ) {
- setDropdownActive(false)
- setFilterLanguageExpanded(false)
- }
- }
-
- if (dropdownActive) {
- document.addEventListener('mousedown', handleClickOutside)
- } else {
- document.removeEventListener('mousedown', handleClickOutside)
- }
-
- return () => {
- document.removeEventListener('mousedown', handleClickOutside)
- }
- }, [buttonRef, dropdownActive])
-
- useEffect(() => {
- const handleEscape = (event: KeyboardEvent) => {
- if (
- popupRef.current &&
- !popupRef.current.contains(event.target as Node) &&
- !filterLanguageExpanded
- ) {
- setDropdownActive(false)
- }
- }
-
- if (dropdownActive && !filterLanguageExpanded) {
- document.addEventListener('keydown', handleEscape)
- } else {
- document.removeEventListener('keydown', handleEscape)
- }
+ useGetAuthUrl(setRedirectUrl, state)
+ useSetRedirectUrl(redirectUrl || '')
- return () => {
- document.removeEventListener('keydown', handleEscape)
- }
- }, [dropdownActive, filterLanguageExpanded])
+ useCloseUserIcon(
+ buttonRef,
+ dropdownActive,
+ filterLanguageExpanded,
+ popupRef,
+ setDropdownActive,
+ setFilterLanguageExpanded
+ )
return (
<>
diff --git a/src/components/streamFeed/StreamFeed.tsx b/src/components/streamFeed/StreamFeed.tsx
index 63a4a33..2fb0b90 100644
--- a/src/components/streamFeed/StreamFeed.tsx
+++ b/src/components/streamFeed/StreamFeed.tsx
@@ -1,4 +1,4 @@
-import { useCallback, useContext, useEffect, useState } from 'react'
+import { useCallback, useContext, useState } from 'react'
import { StreamProps } from '../../types/StreamProps'
import SkeletonFeed from '../skeleton/SkeletonFeed'
@@ -29,6 +29,7 @@ import { getItemFromStorage } from '../../helper/getItemFromStorage'
import { getSearchFilter } from '../../helper/getSearchFilter'
import { getStreams } from '../../helper/getStreams'
import { setItemInStorage } from '../../helper/setItemInStorage'
+import { useLoadStreams } from '../../hooks/useLoadStreams'
const bgColors = [
'bg-gradient-to-tr from-red-400 to-red-800',
@@ -181,16 +182,7 @@ const StreamFeed = () => {
setStreamData,
])
- useEffect(() => {
- loadStreams()
- const refresh = setInterval(() => {
- loadStreams()
- }, 120000)
- return () => {
- clearInterval(refresh)
- }
- }, [loadStreams])
-
+ useLoadStreams(loadStreams)
useFocusTrap(error, filteredStreamData)
if (error) {
diff --git a/src/components/streamFeed/StreamPlayer.tsx b/src/components/streamFeed/StreamPlayer.tsx
index bda80be..a8ccdfe 100644
--- a/src/components/streamFeed/StreamPlayer.tsx
+++ b/src/components/streamFeed/StreamPlayer.tsx
@@ -1,4 +1,7 @@
-import { useEffect, useRef, useState } from 'react'
+import { useRef, useState } from 'react'
+import { useInitPlayer } from '../../hooks/useInitPlayer'
+import { useUpdatePlayerHeight } from '../../hooks/useUpdatePlayerHeight'
+import { useSetPlayerHeight } from '../../hooks/useSetPlayerHeight'
interface StreamPlayerProps {
channel: string
@@ -10,59 +13,9 @@ const StreamPlayer: React.FC = ({ channel }) => {
const [height, setHeight] = useState(0)
const volume = 0.5
- useEffect(() => {
- const updateHeight = () => {
- if (containerRef.current) {
- const width = containerRef.current.offsetWidth
- setHeight(Math.round((width * 9) / 16))
- }
- }
- updateHeight()
- window.addEventListener('resize', updateHeight)
- return () => window.removeEventListener('resize', updateHeight)
- }, [])
-
- useEffect(() => {
- if (height === 0) {
- return
- }
-
- const initPlayer = () => {
- if (playerRef.current) {
- return
- }
-
- if ((window as any).Twitch?.Player) {
- const options = {
- width: '100%',
- height,
- channel,
- muted: true,
- parent: [window.location.hostname],
- }
- playerRef.current = new (window as any).Twitch.Player(
- containerRef.current,
- options
- )
- playerRef.current.setVolume(volume)
- }
- }
-
- if (!(window as any).Twitch) {
- const script = document.createElement('script')
- script.src = 'https://player.twitch.tv/js/embed/v1.js'
- script.addEventListener('load', initPlayer)
- document.body.appendChild(script)
- } else {
- initPlayer()
- }
- }, [channel, height])
-
- useEffect(() => {
- if (playerRef.current && height > 0) {
- playerRef.current._iframe.style.height = `${height}px`
- }
- }, [height])
+ useUpdatePlayerHeight(containerRef, setHeight)
+ useInitPlayer(channel, containerRef, height, playerRef, volume)
+ useSetPlayerHeight(height, playerRef)
return
}
diff --git a/src/components/streamFeed/StreamProfilePicture.tsx b/src/components/streamFeed/StreamProfilePicture.tsx
index f932be8..bed8d7e 100644
--- a/src/components/streamFeed/StreamProfilePicture.tsx
+++ b/src/components/streamFeed/StreamProfilePicture.tsx
@@ -1,4 +1,4 @@
-import { FC, useContext, useEffect, useState } from 'react'
+import { FC, useContext, useState } from 'react'
import {
ContextDisableFocusTrap,
@@ -10,9 +10,9 @@ import {
ContextStreamData,
} from '../../App'
import { getImage } from '../../helper/getImage'
-import { getProfilePicture } from '../../helper/getProfilePicture'
import { getSearchFilter } from '../../helper/getSearchFilter'
import { setItemInStorage } from '../../helper/setItemInStorage'
+import { useFetchImageUrl } from '../../hooks/useFetchImageUrl'
type StreamProfilePictureProps = {
isHeroPicture?: boolean
@@ -90,21 +90,7 @@ const StreamProfilePicture: FC = ({
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [_searchResults, setSearchResults] = contextSearchResults
- useEffect(() => {
- const fetchImageUrl = async () => {
- try {
- const data = await getProfilePicture(user_id || '')
- if (!data) {
- throw new Error()
- } else {
- setImageUrl(data)
- }
- } catch (error: any) {}
- }
- fetchImageUrl()
-
- return () => {}
- }, [user_id])
+ useFetchImageUrl(setImageUrl, user_id)
const handleClick = () => {
setFilteredStreamData({
diff --git a/src/hooks/useCloseSearchResults.ts b/src/hooks/useCloseSearchResults.ts
new file mode 100644
index 0000000..28684c6
--- /dev/null
+++ b/src/hooks/useCloseSearchResults.ts
@@ -0,0 +1,80 @@
+import { useEffect } from 'react'
+
+export const useCloseSearchResults = (
+ anchorRef: React.RefObject,
+ buttonIconRef: React.RefObject,
+ contextScreenWidth: 'MOBILE' | 'TABLET_SMALL' | 'TABLET' | 'DESKTOP',
+ desktopSearchRef: React.RefObject,
+ inputRef: React.RefObject,
+ mobileSearchRef: React.RefObject,
+ searchResultsExpanded: boolean,
+ setSearchResultsExpanded: (value: React.SetStateAction) => void,
+ userIconRef: React.RefObject
+) => {
+ useEffect(() => {
+ const handleClickOutside = (event: MouseEvent) => {
+ if (
+ ((desktopSearchRef.current &&
+ !desktopSearchRef.current.contains(event.target as Node)) ||
+ (mobileSearchRef.current &&
+ !mobileSearchRef.current.contains(
+ event.target as Node
+ ))) &&
+ searchResultsExpanded
+ ) {
+ event.stopPropagation()
+ setSearchResultsExpanded(false)
+ }
+ }
+
+ const handleEscape = (event: KeyboardEvent) => {
+ if (
+ ((desktopSearchRef.current &&
+ desktopSearchRef.current.contains(event.target as Node)) ||
+ (mobileSearchRef.current &&
+ mobileSearchRef.current.contains(
+ event.target as Node
+ ))) &&
+ searchResultsExpanded &&
+ event.key === 'Escape'
+ ) {
+ event.stopPropagation()
+ setSearchResultsExpanded(false)
+ if (!inputRef?.current?.onfocus) {
+ if (
+ contextScreenWidth === 'MOBILE' ||
+ contextScreenWidth === 'TABLET_SMALL'
+ ) {
+ buttonIconRef?.current?.focus()
+ } else {
+ userIconRef?.current?.focus()
+ anchorRef?.current?.focus()
+ }
+ }
+ }
+ }
+
+ if (searchResultsExpanded) {
+ document.addEventListener('keydown', handleEscape)
+ document.addEventListener('mousedown', handleClickOutside)
+ } else {
+ document.removeEventListener('keydown', handleEscape)
+ document.removeEventListener('mousedown', handleClickOutside)
+ }
+
+ return () => {
+ document.removeEventListener('keydown', handleEscape)
+ document.removeEventListener('mousedown', handleClickOutside)
+ }
+ }, [
+ anchorRef,
+ buttonIconRef,
+ contextScreenWidth,
+ desktopSearchRef,
+ inputRef,
+ mobileSearchRef,
+ searchResultsExpanded,
+ setSearchResultsExpanded,
+ userIconRef,
+ ])
+}
diff --git a/src/hooks/useCloseSettingsPopup.ts b/src/hooks/useCloseSettingsPopup.ts
new file mode 100644
index 0000000..726f467
--- /dev/null
+++ b/src/hooks/useCloseSettingsPopup.ts
@@ -0,0 +1,31 @@
+import { useEffect } from 'react'
+
+export const useCloseSettingsPopup = (
+ filterLanguageExpanded: boolean,
+ popupLanguageRef: React.MutableRefObject,
+ setFilterLanguageExpanded: (value: React.SetStateAction) => void
+) => {
+ useEffect(() => {
+ const handleEscape = (event: KeyboardEvent) => {
+ if (
+ popupLanguageRef.current &&
+ !popupLanguageRef.current.contains(event.target as Node) &&
+ filterLanguageExpanded &&
+ event.key === 'Escape'
+ ) {
+ event.stopPropagation()
+ setFilterLanguageExpanded(false)
+ }
+ }
+
+ if (filterLanguageExpanded) {
+ document.addEventListener('keydown', handleEscape)
+ } else {
+ document.removeEventListener('keydown', handleEscape)
+ }
+
+ return () => {
+ document.removeEventListener('keydown', handleEscape)
+ }
+ }, [filterLanguageExpanded, popupLanguageRef, setFilterLanguageExpanded])
+}
diff --git a/src/hooks/useCloseUserIcon.ts b/src/hooks/useCloseUserIcon.ts
new file mode 100644
index 0000000..8d5ef5d
--- /dev/null
+++ b/src/hooks/useCloseUserIcon.ts
@@ -0,0 +1,62 @@
+import { SetStateAction, useEffect } from 'react'
+
+export const useCloseUserIcon = (
+ buttonRef: React.RefObject,
+ dropdownActive: boolean,
+ filterLanguageExpanded: boolean,
+ popupRef: React.MutableRefObject,
+ setDropdownActive: (value: SetStateAction) => void,
+ setFilterLanguageExpanded: (value: SetStateAction) => void
+) => {
+ useEffect(() => {
+ const handleClickOutside = (event: MouseEvent) => {
+ if (
+ popupRef.current &&
+ !popupRef.current.contains(event.target as Node) &&
+ buttonRef.current &&
+ !buttonRef.current.contains(event.target as Node)
+ ) {
+ setDropdownActive(false)
+ setFilterLanguageExpanded(false)
+ }
+ }
+
+ if (dropdownActive) {
+ document.addEventListener('mousedown', handleClickOutside)
+ } else {
+ document.removeEventListener('mousedown', handleClickOutside)
+ }
+
+ return () => {
+ document.removeEventListener('mousedown', handleClickOutside)
+ }
+ }, [
+ buttonRef,
+ dropdownActive,
+ popupRef,
+ setDropdownActive,
+ setFilterLanguageExpanded,
+ ])
+
+ useEffect(() => {
+ const handleEscape = (event: KeyboardEvent) => {
+ if (
+ popupRef.current &&
+ !popupRef.current.contains(event.target as Node) &&
+ !filterLanguageExpanded
+ ) {
+ setDropdownActive(false)
+ }
+ }
+
+ if (dropdownActive && !filterLanguageExpanded) {
+ document.addEventListener('keydown', handleEscape)
+ } else {
+ document.removeEventListener('keydown', handleEscape)
+ }
+
+ return () => {
+ document.removeEventListener('keydown', handleEscape)
+ }
+ }, [dropdownActive, filterLanguageExpanded, popupRef, setDropdownActive])
+}
diff --git a/src/hooks/useDocumentTitle.ts b/src/hooks/useDocumentTitle.ts
new file mode 100644
index 0000000..979ad3e
--- /dev/null
+++ b/src/hooks/useDocumentTitle.ts
@@ -0,0 +1,10 @@
+import { useEffect } from 'react'
+import { getEnglishLanguageName } from '../helper/getEnglishLanguageName'
+
+export const useDocumentTitle = (language: string, seoSearchText: string) => {
+ useEffect(() => {
+ document.title = `Twitch-App | ${getEnglishLanguageName(
+ language
+ )} Livestreams${seoSearchText ? ` | ${seoSearchText}` : ''}`
+ }, [language, seoSearchText])
+}
diff --git a/src/hooks/useFetchImageUrl.ts b/src/hooks/useFetchImageUrl.ts
new file mode 100644
index 0000000..ef3ab6f
--- /dev/null
+++ b/src/hooks/useFetchImageUrl.ts
@@ -0,0 +1,23 @@
+import { useEffect } from 'react'
+import { getProfilePicture } from '../helper/getProfilePicture'
+
+export const useFetchImageUrl = (
+ setImageUrl: (value: React.SetStateAction) => void,
+ user_id: string
+) => {
+ useEffect(() => {
+ const fetchImageUrl = async () => {
+ try {
+ const data = await getProfilePicture(user_id || '')
+ if (!data) {
+ throw new Error()
+ } else {
+ setImageUrl(data)
+ }
+ } catch (error: any) {}
+ }
+ fetchImageUrl()
+
+ return () => {}
+ }, [setImageUrl, user_id])
+}
diff --git a/src/hooks/useFocusInput.ts b/src/hooks/useFocusInput.ts
new file mode 100644
index 0000000..0169067
--- /dev/null
+++ b/src/hooks/useFocusInput.ts
@@ -0,0 +1,14 @@
+import { useEffect } from 'react'
+
+export const useFocusInput = (
+ inputFocussed: boolean,
+ inputRef: React.RefObject | undefined,
+ setInputFocussed: (value: React.SetStateAction) => void
+) => {
+ useEffect(() => {
+ if (inputFocussed && inputRef?.current) {
+ inputRef.current.focus()
+ setInputFocussed(false)
+ }
+ }, [inputFocussed, inputRef, setInputFocussed])
+}
diff --git a/src/hooks/useFocusTrapSearch.ts b/src/hooks/useFocusTrapSearch.ts
new file mode 100644
index 0000000..0baeecf
--- /dev/null
+++ b/src/hooks/useFocusTrapSearch.ts
@@ -0,0 +1,59 @@
+import { useEffect } from 'react'
+
+export const useFocusTrapSearch = (
+ buttonRef: React.MutableRefObject,
+ focusTrapDisabled: boolean,
+ inputRef: React.RefObject | undefined,
+ searchResultsRef: React.MutableRefObject,
+ searchText: string
+) => {
+ useEffect(() => {
+ const handleFocusTrap = (e: KeyboardEvent) => {
+ if (
+ searchText.length === 0 ||
+ e.key !== 'Tab' ||
+ focusTrapDisabled
+ ) {
+ return
+ }
+
+ const focusableElements = [
+ inputRef?.current,
+ buttonRef.current,
+ ...(searchResultsRef.current
+ ? Array.from(
+ searchResultsRef.current.querySelectorAll('button')
+ )
+ : []),
+ ].filter((element) => element !== null) as (
+ | HTMLInputElement
+ | HTMLButtonElement
+ )[]
+
+ if (focusableElements.length === 0) {
+ return
+ }
+
+ const firstElement = focusableElements[0]
+ const lastElement = focusableElements[focusableElements.length - 1]
+
+ if (e.shiftKey) {
+ if (document.activeElement === firstElement) {
+ e.preventDefault()
+ lastElement.focus()
+ }
+ } else {
+ if (document.activeElement === lastElement) {
+ e.preventDefault()
+ firstElement.focus()
+ }
+ }
+ }
+
+ document.addEventListener('keydown', handleFocusTrap)
+
+ return () => {
+ document.removeEventListener('keydown', handleFocusTrap)
+ }
+ }, [buttonRef, focusTrapDisabled, inputRef, searchResultsRef, searchText])
+}
diff --git a/src/hooks/useGetAuthUrl.ts b/src/hooks/useGetAuthUrl.ts
new file mode 100644
index 0000000..d8fe158
--- /dev/null
+++ b/src/hooks/useGetAuthUrl.ts
@@ -0,0 +1,23 @@
+import { SetStateAction, useEffect } from 'react'
+
+export const useGetAuthUrl = (
+ setRedirectUrl: (value: SetStateAction) => void,
+ state: string
+) => {
+ useEffect(() => {
+ const getAuthUrl = async () => {
+ if (state.length) {
+ try {
+ const response = await fetch(
+ `https://twitch-backend.vercel.app/api/auth-url?state=${state}`
+ )
+ const data = await response.json()
+ setRedirectUrl(data.url)
+ } catch (error) {
+ console.error('Error fetching auth URL:', error)
+ }
+ }
+ }
+ getAuthUrl()
+ }, [setRedirectUrl, state])
+}
diff --git a/src/hooks/useHideMobileSearch.ts b/src/hooks/useHideMobileSearch.ts
new file mode 100644
index 0000000..33a9161
--- /dev/null
+++ b/src/hooks/useHideMobileSearch.ts
@@ -0,0 +1,19 @@
+import { useEffect } from 'react'
+
+export const useHideMobileSearch = (
+ contextScreenWidth: 'MOBILE' | 'TABLET_SMALL' | 'TABLET' | 'DESKTOP',
+ inputFocussed: boolean,
+ setAriaPressed: (value: React.SetStateAction) => void,
+ setHideSearch: React.Dispatch>
+) => {
+ useEffect(() => {
+ if (
+ (contextScreenWidth === 'MOBILE' ||
+ contextScreenWidth === 'TABLET_SMALL') &&
+ inputFocussed
+ ) {
+ setHideSearch(false)
+ setAriaPressed(true)
+ }
+ }, [contextScreenWidth, inputFocussed, setAriaPressed, setHideSearch])
+}
diff --git a/src/hooks/useInitPlayer.ts b/src/hooks/useInitPlayer.ts
new file mode 100644
index 0000000..c57ec9a
--- /dev/null
+++ b/src/hooks/useInitPlayer.ts
@@ -0,0 +1,45 @@
+import { useEffect } from 'react'
+
+export const useInitPlayer = (
+ channel: string,
+ containerRef: React.MutableRefObject,
+ height: number,
+ playerRef: React.MutableRefObject,
+ volume: number
+) => {
+ useEffect(() => {
+ if (height === 0) {
+ return
+ }
+
+ const initPlayer = () => {
+ if (playerRef.current) {
+ return
+ }
+
+ if ((window as any).Twitch?.Player) {
+ const options = {
+ width: '100%',
+ height,
+ channel,
+ muted: true,
+ parent: [window.location.hostname],
+ }
+ playerRef.current = new (window as any).Twitch.Player(
+ containerRef.current,
+ options
+ )
+ playerRef.current.setVolume(volume)
+ }
+ }
+
+ if (!(window as any).Twitch) {
+ const script = document.createElement('script')
+ script.src = 'https://player.twitch.tv/js/embed/v1.js'
+ script.addEventListener('load', initPlayer)
+ document.body.appendChild(script)
+ } else {
+ initPlayer()
+ }
+ }, [channel, containerRef, height, playerRef, volume])
+}
diff --git a/src/hooks/useLoadStreams.ts b/src/hooks/useLoadStreams.ts
new file mode 100644
index 0000000..82b3dae
--- /dev/null
+++ b/src/hooks/useLoadStreams.ts
@@ -0,0 +1,13 @@
+import { useEffect } from 'react'
+
+export const useLoadStreams = (loadStreams: () => Promise) => {
+ useEffect(() => {
+ loadStreams()
+ const refresh = setInterval(() => {
+ loadStreams()
+ }, 120000)
+ return () => {
+ clearInterval(refresh)
+ }
+ }, [loadStreams])
+}
diff --git a/src/hooks/useLogUserOut.ts b/src/hooks/useLogUserOut.ts
new file mode 100644
index 0000000..8eb816a
--- /dev/null
+++ b/src/hooks/useLogUserOut.ts
@@ -0,0 +1,22 @@
+import { useEffect } from 'react'
+import { UserProps } from '../types/UserProps'
+
+export const useLogUserOut = (
+ fetchUser: () => Promise,
+ user: UserProps | null
+) => {
+ useEffect(() => {
+ const isLoggedIn = sessionStorage.getItem('twitch_logged_in')
+ isLoggedIn === 'true' && !user && fetchUser()
+ const timer = setTimeout(
+ () =>
+ isLoggedIn === 'false' &&
+ (sessionStorage.removeItem('twitch_logged_in'),
+ sessionStorage.removeItem('twitch_access_state'),
+ sessionStorage.removeItem('twitch_access_token'),
+ sessionStorage.removeItem('twitch_user')),
+ 0
+ )
+ return () => clearTimeout(timer)
+ }, [fetchUser, user])
+}
diff --git a/src/hooks/useNavigationScrollY.ts b/src/hooks/useNavigationScrollY.ts
new file mode 100644
index 0000000..202152f
--- /dev/null
+++ b/src/hooks/useNavigationScrollY.ts
@@ -0,0 +1,37 @@
+import { useEffect } from 'react'
+
+export const useNavigationScrollY = (
+ blockOpacity: boolean,
+ navOpacity: string,
+ setNavOpacity: (value: React.SetStateAction) => void
+) => {
+ useEffect(() => {
+ let lastScrollY = window.scrollY
+ let timer: NodeJS.Timeout
+
+ const handleScroll = () => {
+ if (window.scrollY === 0) {
+ setNavOpacity('opacity-100')
+ } else if (window.scrollY < lastScrollY && !blockOpacity) {
+ setNavOpacity('opacity-75')
+ } else if (!blockOpacity) {
+ setNavOpacity('opacity-95')
+ }
+ lastScrollY = window.scrollY
+
+ clearTimeout(timer)
+ if (navOpacity !== 'opacity-100') {
+ timer = setTimeout(() => {
+ setNavOpacity('opacity-100')
+ }, 500)
+ }
+ }
+
+ window.addEventListener('scroll', handleScroll)
+
+ return () => {
+ window.removeEventListener('scroll', handleScroll)
+ clearTimeout(timer)
+ }
+ }, [blockOpacity, navOpacity, setNavOpacity])
+}
diff --git a/src/hooks/useSetPlayerHeight.ts b/src/hooks/useSetPlayerHeight.ts
new file mode 100644
index 0000000..a0b1ddd
--- /dev/null
+++ b/src/hooks/useSetPlayerHeight.ts
@@ -0,0 +1,12 @@
+import { useEffect } from 'react'
+
+export const useSetPlayerHeight = (
+ height: number,
+ playerRef: React.MutableRefObject
+) => {
+ useEffect(() => {
+ if (playerRef.current && height > 0) {
+ playerRef.current._iframe.style.height = `${height}px`
+ }
+ }, [height, playerRef])
+}
diff --git a/src/hooks/useSetRedirectUrl.ts b/src/hooks/useSetRedirectUrl.ts
new file mode 100644
index 0000000..f4f9f34
--- /dev/null
+++ b/src/hooks/useSetRedirectUrl.ts
@@ -0,0 +1,9 @@
+import { useEffect } from 'react'
+
+export const useSetRedirectUrl = (redirectUrl: string) => {
+ useEffect(() => {
+ if (redirectUrl) {
+ window.location.href = redirectUrl
+ }
+ }, [redirectUrl])
+}
diff --git a/src/hooks/useSettingsPopupFocusTrap.ts b/src/hooks/useSettingsPopupFocusTrap.ts
new file mode 100644
index 0000000..1b4f71f
--- /dev/null
+++ b/src/hooks/useSettingsPopupFocusTrap.ts
@@ -0,0 +1,44 @@
+import { useEffect } from 'react'
+
+export const useSettingsPopupFocusTrap = (
+ filterLanguageExpanded: boolean,
+ popupRef: React.MutableRefObject
+) => {
+ useEffect(() => {
+ if (filterLanguageExpanded) {
+ return
+ }
+
+ const buttons = popupRef.current?.querySelectorAll('button')
+ if (!buttons || buttons.length === 0) {
+ return
+ }
+
+ const firstButton = buttons[0]
+ const lastButton = buttons[buttons.length - 1]
+
+ const handleFocusTrap = (e: KeyboardEvent) => {
+ if (e.key !== 'Tab') {
+ return
+ }
+
+ if (e.shiftKey) {
+ if (document.activeElement === firstButton) {
+ e.preventDefault()
+ lastButton.focus()
+ }
+ } else {
+ if (document.activeElement === lastButton) {
+ e.preventDefault()
+ firstButton.focus()
+ }
+ }
+ }
+
+ document.addEventListener('keydown', handleFocusTrap)
+
+ return () => {
+ document.removeEventListener('keydown', handleFocusTrap)
+ }
+ }, [filterLanguageExpanded, popupRef])
+}
diff --git a/src/hooks/useUpdatePlayerHeight.ts b/src/hooks/useUpdatePlayerHeight.ts
new file mode 100644
index 0000000..abdcb5e
--- /dev/null
+++ b/src/hooks/useUpdatePlayerHeight.ts
@@ -0,0 +1,18 @@
+import { useEffect } from 'react'
+
+export const useUpdatePlayerHeight = (
+ containerRef: React.MutableRefObject,
+ setHeight: (value: React.SetStateAction) => void
+) => {
+ useEffect(() => {
+ const updateHeight = () => {
+ if (containerRef.current) {
+ const width = containerRef.current.offsetWidth
+ setHeight(Math.round((width * 9) / 16))
+ }
+ }
+ updateHeight()
+ window.addEventListener('resize', updateHeight)
+ return () => window.removeEventListener('resize', updateHeight)
+ }, [containerRef, setHeight])
+}