|
@@ -1,20 +1,27 @@
|
|
import React, { useEffect, useState } from 'react'
|
|
import React, { useEffect, useState } from 'react'
|
|
import { useParams } from 'react-router-dom'
|
|
import { useParams } from 'react-router-dom'
|
|
|
|
|
|
-import { useChannel, useFollowChannel, useUnfollowChannel } from '@/api/hooks'
|
|
|
|
-import { InfiniteVideoGrid, ViewWrapper } from '@/components'
|
|
|
|
|
|
+import { useChannel, useFollowChannel, useUnfollowChannel, useVideosConnection } from '@/api/hooks'
|
|
|
|
+import { VideoOrderByInput } from '@/api/queries'
|
|
|
|
+import { LimitedWidthContainer, VideoTile, ViewWrapper } from '@/components'
|
|
|
|
+import { SORT_OPTIONS } from '@/config/sorting'
|
|
import { AssetType, useAsset, useDialog, usePersonalDataStore } from '@/providers'
|
|
import { AssetType, useAsset, useDialog, usePersonalDataStore } from '@/providers'
|
|
-import { Button, ChannelCover } from '@/shared/components'
|
|
|
|
|
|
+import { ChannelCover, Grid, Pagination, Select, Tabs, Text } from '@/shared/components'
|
|
|
|
+import { SvgGlyphCheck, SvgGlyphPlus } from '@/shared/icons'
|
|
import { transitions } from '@/shared/theme'
|
|
import { transitions } from '@/shared/theme'
|
|
import { Logger } from '@/utils/logger'
|
|
import { Logger } from '@/utils/logger'
|
|
import { formatNumberShort } from '@/utils/number'
|
|
import { formatNumberShort } from '@/utils/number'
|
|
|
|
|
|
|
|
+import { ChannelAbout } from './ChannelAbout'
|
|
import {
|
|
import {
|
|
- Header,
|
|
|
|
|
|
+ PaginationContainer,
|
|
|
|
+ SortContainer,
|
|
|
|
+ StyledButton,
|
|
StyledButtonContainer,
|
|
StyledButtonContainer,
|
|
StyledChannelLink,
|
|
StyledChannelLink,
|
|
SubTitle,
|
|
SubTitle,
|
|
SubTitleSkeletonLoader,
|
|
SubTitleSkeletonLoader,
|
|
|
|
+ TabsContainer,
|
|
Title,
|
|
Title,
|
|
TitleContainer,
|
|
TitleContainer,
|
|
TitleSection,
|
|
TitleSection,
|
|
@@ -22,6 +29,10 @@ import {
|
|
VideoSection,
|
|
VideoSection,
|
|
} from './ChannelView.style'
|
|
} from './ChannelView.style'
|
|
|
|
|
|
|
|
+const TABS = ['Videos', 'Information'] as const
|
|
|
|
+const INITIAL_FIRST = 50
|
|
|
|
+const INITIAL_VIDEOS_PER_ROW = 4
|
|
|
|
+const ROWS_AMOUNT = 4
|
|
export const ChannelView: React.FC = () => {
|
|
export const ChannelView: React.FC = () => {
|
|
const [openUnfollowDialog, closeUnfollowDialog] = useDialog()
|
|
const [openUnfollowDialog, closeUnfollowDialog] = useDialog()
|
|
const { id } = useParams()
|
|
const { id } = useParams()
|
|
@@ -31,11 +42,26 @@ export const ChannelView: React.FC = () => {
|
|
const followedChannels = usePersonalDataStore((state) => state.followedChannels)
|
|
const followedChannels = usePersonalDataStore((state) => state.followedChannels)
|
|
const updateChannelFollowing = usePersonalDataStore((state) => state.actions.updateChannelFollowing)
|
|
const updateChannelFollowing = usePersonalDataStore((state) => state.actions.updateChannelFollowing)
|
|
const [isFollowing, setFollowing] = useState<boolean>()
|
|
const [isFollowing, setFollowing] = useState<boolean>()
|
|
|
|
+ const [currentVideosTab, setCurrentVideosTab] = useState(0)
|
|
|
|
+ const currentTabName = TABS[currentVideosTab]
|
|
|
|
+ const [sortVideosBy, setSortVideosBy] = useState<VideoOrderByInput | undefined>(VideoOrderByInput.CreatedAtDesc)
|
|
|
|
+ const [videosPerRow, setVideosPerRow] = useState(INITIAL_VIDEOS_PER_ROW)
|
|
const { url: coverPhotoUrl } = useAsset({
|
|
const { url: coverPhotoUrl } = useAsset({
|
|
entity: channel,
|
|
entity: channel,
|
|
assetType: AssetType.COVER,
|
|
assetType: AssetType.COVER,
|
|
})
|
|
})
|
|
-
|
|
|
|
|
|
+ const { currentPage, setCurrentPage } = usePagination(0)
|
|
|
|
+ const { edges, totalCount, loading: loadingVideos, error: videosError, refetch } = useVideosConnection(
|
|
|
|
+ {
|
|
|
|
+ first: INITIAL_FIRST,
|
|
|
|
+ orderBy: sortVideosBy,
|
|
|
|
+ where: {
|
|
|
|
+ channelId_eq: id,
|
|
|
|
+ isPublic_eq: true,
|
|
|
|
+ },
|
|
|
|
+ },
|
|
|
|
+ { notifyOnNetworkStatusChange: true, fetchPolicy: 'cache-and-network' }
|
|
|
|
+ )
|
|
useEffect(() => {
|
|
useEffect(() => {
|
|
const isFollowing = followedChannels.some((channel) => channel.id === id)
|
|
const isFollowing = followedChannels.some((channel) => channel.id === id)
|
|
setFollowing(isFollowing)
|
|
setFollowing(isFollowing)
|
|
@@ -77,20 +103,76 @@ export const ChannelView: React.FC = () => {
|
|
Logger.warn('Failed to update Channel following', { error })
|
|
Logger.warn('Failed to update Channel following', { error })
|
|
}
|
|
}
|
|
}
|
|
}
|
|
- if (error) {
|
|
|
|
|
|
+ if (videosError) {
|
|
|
|
+ throw videosError
|
|
|
|
+ } else if (error) {
|
|
throw error
|
|
throw error
|
|
}
|
|
}
|
|
|
|
|
|
|
|
+ const handleSorting = (value?: VideoOrderByInput | null | undefined) => {
|
|
|
|
+ if (value) {
|
|
|
|
+ setSortVideosBy(value)
|
|
|
|
+ refetch({ orderBy: value })
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ const handleOnResizeGrid = (sizes: number[]) => setVideosPerRow(sizes.length)
|
|
|
|
+ const handleChangePage = (page: number) => {
|
|
|
|
+ setCurrentPage(page)
|
|
|
|
+ }
|
|
|
|
+ const handleSetCurrentTab = async (tab: number) => {
|
|
|
|
+ setCurrentVideosTab(tab)
|
|
|
|
+ }
|
|
|
|
+ const videosPerPage = ROWS_AMOUNT * videosPerRow
|
|
|
|
+
|
|
|
|
+ const videos = edges
|
|
|
|
+ ?.map((edge) => edge.node)
|
|
|
|
+ .slice(currentPage * videosPerPage, currentPage * videosPerPage + videosPerPage)
|
|
|
|
+ const placeholderItems = Array.from(
|
|
|
|
+ { length: loadingVideos ? videosPerPage - (videos ? videos.length : 0) : 0 },
|
|
|
|
+ () => ({
|
|
|
|
+ id: undefined,
|
|
|
|
+ progress: undefined,
|
|
|
|
+ })
|
|
|
|
+ )
|
|
|
|
+ const videosWithPlaceholders = [...(videos || []), ...placeholderItems]
|
|
|
|
+ const mappedTabs = TABS.map((tab) => ({ name: tab, badgeNumber: 0 }))
|
|
|
|
+
|
|
|
|
+ const TabContent = () => {
|
|
|
|
+ switch (currentTabName) {
|
|
|
|
+ case 'Videos':
|
|
|
|
+ return (
|
|
|
|
+ <>
|
|
|
|
+ <VideoSection className={transitions.names.slide}>
|
|
|
|
+ <Grid maxColumns={null} onResize={handleOnResizeGrid}>
|
|
|
|
+ {videosWithPlaceholders.map((video, idx) => (
|
|
|
|
+ <VideoTile key={idx} id={video.id} showChannel={false} />
|
|
|
|
+ ))}
|
|
|
|
+ </Grid>
|
|
|
|
+ </VideoSection>
|
|
|
|
+ <PaginationContainer>
|
|
|
|
+ <Pagination
|
|
|
|
+ onChangePage={handleChangePage}
|
|
|
|
+ page={currentPage}
|
|
|
|
+ itemsPerPage={videosPerPage}
|
|
|
|
+ totalCount={totalCount}
|
|
|
|
+ />
|
|
|
|
+ </PaginationContainer>
|
|
|
|
+ </>
|
|
|
|
+ )
|
|
|
|
+ case 'Information':
|
|
|
|
+ return <ChannelAbout />
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
if (!loading && !channel) {
|
|
if (!loading && !channel) {
|
|
return <span>Channel not found</span>
|
|
return <span>Channel not found</span>
|
|
}
|
|
}
|
|
-
|
|
|
|
return (
|
|
return (
|
|
<ViewWrapper>
|
|
<ViewWrapper>
|
|
- <Header>
|
|
|
|
- <ChannelCover assetUrl={coverPhotoUrl} />
|
|
|
|
|
|
+ <ChannelCover assetUrl={coverPhotoUrl} />
|
|
|
|
+ <LimitedWidthContainer>
|
|
<TitleSection className={transitions.names.slide}>
|
|
<TitleSection className={transitions.names.slide}>
|
|
- <StyledChannelLink id={channel?.id} avatarSize="view" hideHandle noLink />
|
|
|
|
|
|
+ <StyledChannelLink id={channel?.id} avatarSize="channel" hideHandle noLink />
|
|
<TitleContainer>
|
|
<TitleContainer>
|
|
{channel ? (
|
|
{channel ? (
|
|
<>
|
|
<>
|
|
@@ -105,15 +187,36 @@ export const ChannelView: React.FC = () => {
|
|
)}
|
|
)}
|
|
</TitleContainer>
|
|
</TitleContainer>
|
|
<StyledButtonContainer>
|
|
<StyledButtonContainer>
|
|
- <Button variant={isFollowing ? 'secondary' : 'primary'} onClick={handleFollow} size="large">
|
|
|
|
|
|
+ <StyledButton
|
|
|
|
+ icon={isFollowing ? <SvgGlyphCheck /> : <SvgGlyphPlus />}
|
|
|
|
+ variant={isFollowing ? 'secondary' : 'primary'}
|
|
|
|
+ onClick={handleFollow}
|
|
|
|
+ size="large"
|
|
|
|
+ >
|
|
{isFollowing ? 'Unfollow' : 'Follow'}
|
|
{isFollowing ? 'Unfollow' : 'Follow'}
|
|
- </Button>
|
|
|
|
|
|
+ </StyledButton>
|
|
</StyledButtonContainer>
|
|
</StyledButtonContainer>
|
|
</TitleSection>
|
|
</TitleSection>
|
|
- </Header>
|
|
|
|
- <VideoSection className={transitions.names.slide}>
|
|
|
|
- <InfiniteVideoGrid channelId={id} showChannel={false} />
|
|
|
|
- </VideoSection>
|
|
|
|
|
|
+ <TabsContainer>
|
|
|
|
+ <Tabs initialIndex={0} tabs={mappedTabs} onSelectTab={handleSetCurrentTab} />
|
|
|
|
+ {currentTabName === 'Videos' && (
|
|
|
|
+ <SortContainer>
|
|
|
|
+ <Text variant="body2">Sort by</Text>
|
|
|
|
+ <Select helperText={null} value={sortVideosBy} items={SORT_OPTIONS} onChange={handleSorting} />
|
|
|
|
+ </SortContainer>
|
|
|
|
+ )}
|
|
|
|
+ </TabsContainer>
|
|
|
|
+ <TabContent />
|
|
|
|
+ </LimitedWidthContainer>
|
|
</ViewWrapper>
|
|
</ViewWrapper>
|
|
)
|
|
)
|
|
}
|
|
}
|
|
|
|
+
|
|
|
|
+const usePagination = (currentTab: number) => {
|
|
|
|
+ const [currentPage, setCurrentPage] = useState(0)
|
|
|
|
+ // reset the pagination when changing tabs
|
|
|
|
+ useEffect(() => {
|
|
|
|
+ setCurrentPage(0)
|
|
|
|
+ }, [currentTab])
|
|
|
|
+ return { currentPage, setCurrentPage }
|
|
|
|
+}
|