import { useCallback, useEffect, useState, useRef } from 'react'; import { bound, clsm, range } from '../../utils'; import { BREAKPOINTS, MAX_AVATAR_COUNT } from '../../constants'; import { channelDirectory as $channelDirectoryContent } from '../../content'; import { ChevronLeft, ChevronRight } from '../../assets/icons'; import { getAvatarSrc } from '../../helpers'; import { useNotif } from '../../contexts/Notification'; import { useResponsiveDevice } from '../../contexts/ResponsiveDevice'; import { useUser } from '../../contexts/User'; import Button from '../../components/Button'; import DataUnavailable from './DataUnavailable'; import FollowedUserButton from './FollowedUserButton'; import Spinner from '../../components/Spinner'; import useForceLoader from '../../hooks/useForceLoader'; import usePrevious from '../../hooks/usePrevious'; import ViewAllButton from './ViewAllButton'; const $content = $channelDirectoryContent.following_section; const $channelDirectoryNotifications = $channelDirectoryContent.notification; export const FIRST_ITEM_IN_FRAME = 'firstItemInFrame'; export const LAST_ITEM_IN_FRAME = 'lastItemInFrame'; const CAROUSEL_BUTTON_CLASSES = [ 'bg-lightMode-gray-extraLight', 'h-auto', 'min-w-[auto]', 'p-1.5' ]; const SECTION_MIN_HEIGHT_CLASSES = [ 'min-h-[204px]', 'md:min-h-[174px]', 'sm:min-h-[146px]' ]; const SECTION_CENTERED_CONTENT_BASE_CLASSES = [ 'flex-col', 'flex', 'grow', 'h-auto', 'items-center', 'justify-center', 'left-0', 'h-full', 'relative', 'static', 'top-0', 'w-full', SECTION_MIN_HEIGHT_CLASSES ]; const isPreviousFrame = (frameIndex, currentFrameIndex) => frameIndex === currentFrameIndex - 1; const isNextFrame = (frameIndex, currentFrameIndex) => frameIndex === currentFrameIndex + 1; const FollowingSection = () => { const { fetchUserFollowingList: tryAgainFn, hasErrorFetchingFollowingList: hasFetchError, isSessionValid, userData } = useUser(); const [selectedFrameIndex, setSelectedFrameIndex] = useState(0); const [isSlideAnimationDisabled, setIsSlideAnimationDisabled] = useState(false); const isLoadingForced = useForceLoader(); const { currentBreakpoint, isMobileView } = useResponsiveDevice(); const { notifyError } = useNotif(); const prevBreakpoint = usePrevious(currentBreakpoint); const { followingList } = userData || {}; const hasFollowingListData = !!followingList?.length; const isLoading = (followingList === undefined && !hasFetchError) || isLoadingForced; const shouldShowFollowingListData = !isLoading && hasFollowingListData; const shouldShowTryAgainButton = hasFetchError && !isMobileView; const shouldShowViewAllButton = followingList?.length > MAX_AVATAR_COUNT; let sectionList = followingList; const firstAndLastItemInFrameRef = useRef({}); if (shouldShowViewAllButton) sectionList = followingList?.slice(0, MAX_AVATAR_COUNT); // Carousel parameters - START let avatarsPerFrame = 5; if (currentBreakpoint < BREAKPOINTS.lg) avatarsPerFrame = 4; if (currentBreakpoint < BREAKPOINTS.sm) avatarsPerFrame = 3; if (currentBreakpoint === BREAKPOINTS.xxs) avatarsPerFrame = 2; const carouselFramesCount = Math.ceil(sectionList?.length / avatarsPerFrame); const prevLeftMostAvatarIndex = usePrevious( selectedFrameIndex * avatarsPerFrame ); // Carousel parameters - END const prevButtonHandler = useCallback(() => { setSelectedFrameIndex((prev) => bound(prev - 1, 0)); }, []); const nextButtonHandler = useCallback(() => { setSelectedFrameIndex((prev) => bound(prev + 1, 0, carouselFramesCount - 1) ); }, [carouselFramesCount]); /** * The following effect ensures that, after a viewport resizing occurs, * the new selected frame index is set based on the previously selected frame index */ useEffect(() => { if ( prevBreakpoint !== currentBreakpoint && typeof prevLeftMostAvatarIndex === 'number' ) setSelectedFrameIndex( Math.floor(prevLeftMostAvatarIndex / avatarsPerFrame) ); }, [ avatarsPerFrame, currentBreakpoint, prevBreakpoint, prevLeftMostAvatarIndex ]); useEffect(() => { if (hasFetchError) notifyError($channelDirectoryNotifications.error.error_loading_channels); }, [hasFetchError, notifyError]); useEffect(() => { const handleTabbing = (e) => { if (e.code === 'Tab') { setIsSlideAnimationDisabled(true); if (e.shiftKey) { if ( firstAndLastItemInFrameRef.current[avatarsPerFrame][ FIRST_ITEM_IN_FRAME ].has(document.activeElement.href) ) { prevButtonHandler(); } } else { if ( firstAndLastItemInFrameRef.current[avatarsPerFrame][ LAST_ITEM_IN_FRAME ].has(document.activeElement.href) ) { nextButtonHandler(); } } } }; document.addEventListener('keydown', handleTabbing); return () => document.removeEventListener('keydown', handleTabbing); }, [ nextButtonHandler, prevButtonHandler, selectedFrameIndex, avatarsPerFrame, isSlideAnimationDisabled ]); /** * If the user is not logged hide the following summary section. */ if (!isSessionValid) return null; return (

{$content.title}

{shouldShowFollowingListData && (
)}
{shouldShowFollowingListData && (
{range(carouselFramesCount).map((frameIndex) => { const framePositon = frameIndex !== 0; const leftMostAvatarIndex = frameIndex * avatarsPerFrame; const rightMostAvatarIndex = leftMostAvatarIndex + avatarsPerFrame; let translateOffset = 0; const selectedSectionList = sectionList.slice( leftMostAvatarIndex, rightMostAvatarIndex ); if (isPreviousFrame(frameIndex, selectedFrameIndex)) translateOffset = -32; if (isNextFrame(frameIndex, selectedFrameIndex)) translateOffset = 32; return (
{selectedSectionList.map((channelData, i) => { const { color, username, isLive } = channelData; const isLastItemInFrame = selectedSectionList.length - 1 === i; const isFirstItemInFrame = i === 0 && frameIndex > 0; return ( ); })} {frameIndex === carouselFramesCount - 1 && !hasFetchError && shouldShowViewAllButton && }
); })}
)} {!isLoading && !hasFollowingListData && ( )} {isLoading && (
)}
); }; export default FollowingSection;