Procházet zdrojové kódy

Merge pull request #4093 from WRadoslaw/feature/channels-section

🍶 Channels section
attemka před 1 rokem
rodič
revize
356e57978f
21 změnil soubory, kde provedl 174 přidání a 57 odebrání
  1. 1 0
      packages/atlas/src/components/Section/Section.stories.tsx
  2. 1 0
      packages/atlas/src/components/Section/SectionFooter/SectionFooter.stories.tsx
  3. 8 1
      packages/atlas/src/components/Section/SectionFooter/SectionFooter.tsx
  4. 1 1
      packages/atlas/src/components/_buttons/CallToActionButton/CallToActionButton.stories.tsx
  5. 11 5
      packages/atlas/src/components/_buttons/CallToActionButton/CallToActionButton.styles.ts
  6. 0 8
      packages/atlas/src/components/_buttons/CallToActionButton/CallToActionButton.tsx
  7. 17 7
      packages/atlas/src/components/_channel/ChannelCard/ChannelCard.stories.tsx
  8. 45 0
      packages/atlas/src/components/_channel/ChannelsSection/ChannelsSection.stories.tsx
  9. 69 0
      packages/atlas/src/components/_channel/ChannelsSection/ChannelsSection.tsx
  10. 1 0
      packages/atlas/src/components/_channel/ChannelsSection/index.ts
  11. 6 0
      packages/atlas/src/components/_inputs/ToggleButtonGroup/ToggleButtonGroup.styles.ts
  12. 3 3
      packages/atlas/src/components/_inputs/ToggleButtonGroup/ToggleButtonGroup.tsx
  13. 0 7
      packages/atlas/src/components/_navigation/SidenavViewer/SidenavViewer.tsx
  14. 1 1
      packages/atlas/src/components/_templates/VideoContentTemplate.tsx
  15. 0 16
      packages/atlas/src/views/global/YppLandingView/YppFooter.styles.ts
  16. 4 4
      packages/atlas/src/views/global/YppLandingView/YppFooter.tsx
  17. 2 0
      packages/atlas/src/views/viewer/ChannelsView/ChannelsView.tsx
  18. 1 1
      packages/atlas/src/views/viewer/HomeView.tsx
  19. 1 1
      packages/atlas/src/views/viewer/NewView/NewView.tsx
  20. 1 1
      packages/atlas/src/views/viewer/PopularView/PopularView.tsx
  21. 1 1
      packages/atlas/src/views/viewer/VideoView/VideoView.tsx

+ 1 - 0
packages/atlas/src/components/Section/Section.stories.tsx

@@ -237,6 +237,7 @@ const DefaultTemplate: StoryFn<SectionProps> = () => {
         footerProps={{
           type: 'infinite',
           fetchMore: async () => setSecondPlaceholdersCount((count) => count + 8),
+          reachedEnd: secondPlaceholderItems.length > 40,
         }}
       />
     </div>

+ 1 - 0
packages/atlas/src/components/Section/SectionFooter/SectionFooter.stories.tsx

@@ -88,6 +88,7 @@ const InfiniteTemplate: StoryFn<{ type: 'infinite' | 'load' }> = ({ type }) => {
       </Grid>
       <SectionFooter
         type={type}
+        reachedEnd={items > 50}
         label="Load more boxes"
         fetchMore={() =>
           new Promise((res) => {

+ 8 - 1
packages/atlas/src/components/Section/SectionFooter/SectionFooter.tsx

@@ -16,6 +16,7 @@ type SectionFooterLoadProps = {
   type: 'load'
   label: string
   fetchMore: () => Promise<void>
+  reachedEnd: boolean
 }
 
 type SectionFooterPaginationProps = {
@@ -25,6 +26,7 @@ type SectionFooterPaginationProps = {
 type SectionFooterInfiniteLoadingProps = {
   type: 'infinite'
   fetchMore: () => Promise<void>
+  reachedEnd: boolean
 }
 
 export type SectionFooterProps =
@@ -43,7 +45,12 @@ export const SectionFooter = (props: SectionFooterProps) => {
   const { ref, inView } = useInView()
 
   useEffect(() => {
-    if ((props.type === 'infinite' || (props.type === 'load' && isSwitchedToInfinite)) && inView && !isLoading) {
+    if (
+      (props.type === 'infinite' || (props.type === 'load' && isSwitchedToInfinite)) &&
+      !props.reachedEnd &&
+      inView &&
+      !isLoading
+    ) {
       setIsLoading(true)
       props.fetchMore().finally(() => setIsLoading(false))
     }

+ 1 - 1
packages/atlas/src/components/_buttons/CallToActionButton/CallToActionButton.stories.tsx

@@ -11,7 +11,7 @@ export default {
 
 const Template: StoryFn<CallToActionButtonProps> = (args) => {
   return (
-    <CallToActionWrapper>
+    <CallToActionWrapper itemsCount={1}>
       <CallToActionButton {...args} />
     </CallToActionWrapper>
   )

+ 11 - 5
packages/atlas/src/components/_buttons/CallToActionButton/CallToActionButton.styles.ts

@@ -15,13 +15,19 @@ const mappedColors = {
   white: cVar('colorCoreNeutral50'),
 }
 
-export const CallToActionWrapper = styled.div`
-  margin-top: ${sizes(32)};
+export const CallToActionWrapper = styled.div<{ itemsCount: number }>`
+  display: grid;
+  gap: ${sizes(4)};
+  padding-bottom: ${sizes(16)};
+  justify-items: center;
 
   ${media.md} {
-    display: grid;
-    grid-template-columns: 1fr 1fr 1fr;
-    grid-column-gap: ${sizes(6)};
+    margin: 0 auto;
+    justify-content: center;
+    grid-template-columns: repeat(auto-fit, minmax(219px, 1fr));
+    max-width: ${({ itemsCount }) => `calc(${itemsCount - 1} * ${sizes(6)} + 419px * ${itemsCount}) `};
+    gap: ${sizes(6)};
+    padding-bottom: ${sizes(24)};
   }
 `
 type IconWrapperProps = {

+ 0 - 8
packages/atlas/src/components/_buttons/CallToActionButton/CallToActionButton.tsx

@@ -4,7 +4,6 @@ import { FC, MouseEvent, ReactNode } from 'react'
 import {
   SvgActionChevronR,
   SvgActionNewTab,
-  SvgSidebarChannels,
   SvgSidebarExplore,
   SvgSidebarHome,
   SvgSidebarNew,
@@ -64,13 +63,6 @@ export const CTA_MAP: Record<string, CallToActionButtonProps> = {
     colorVariant: 'green',
     icon: <SvgSidebarNew />,
   },
-  channels: {
-    label: 'Browse channels',
-    to: absoluteRoutes.viewer.channels(),
-    colorVariant: 'blue',
-    iconColorVariant: 'lightBlue',
-    icon: <SvgSidebarChannels />,
-  },
   popular: {
     label: `Popular on ${atlasConfig.general.appName}`,
     to: absoluteRoutes.viewer.popular(),

+ 17 - 7
packages/atlas/src/components/_channel/ChannelCard/ChannelCard.stories.tsx

@@ -1,5 +1,6 @@
 import { ApolloClient, ApolloProvider, InMemoryCache } from '@apollo/client'
 import { Meta, StoryFn } from '@storybook/react'
+import { QueryClient, QueryClientProvider } from 'react-query'
 import { BrowserRouter } from 'react-router-dom'
 
 // import { createApolloClient } from '@/api'
@@ -29,16 +30,25 @@ export default {
   decorators: [
     (Story) => {
       const apolloClient = new ApolloClient({ cache: new InMemoryCache() })
+      const queryClient = new QueryClient({
+        defaultOptions: {
+          queries: {
+            refetchOnWindowFocus: false,
+          },
+        },
+      })
       return (
         <BrowserRouter>
           <ApolloProvider client={apolloClient}>
-            <OverlayManagerProvider>
-              <OperatorsContextProvider>
-                <ConfirmationModalProvider>
-                  <Story />
-                </ConfirmationModalProvider>
-              </OperatorsContextProvider>
-            </OverlayManagerProvider>
+            <QueryClientProvider client={queryClient}>
+              <OverlayManagerProvider>
+                <OperatorsContextProvider>
+                  <ConfirmationModalProvider>
+                    <Story />
+                  </ConfirmationModalProvider>
+                </OperatorsContextProvider>
+              </OverlayManagerProvider>
+            </QueryClientProvider>
           </ApolloProvider>
         </BrowserRouter>
       )

+ 45 - 0
packages/atlas/src/components/_channel/ChannelsSection/ChannelsSection.stories.tsx

@@ -0,0 +1,45 @@
+import { ApolloClient, ApolloProvider, InMemoryCache } from '@apollo/client'
+import { Meta, StoryFn } from '@storybook/react'
+import { QueryClient, QueryClientProvider } from 'react-query'
+import { BrowserRouter } from 'react-router-dom'
+
+import { ChannelsSection } from '@/components/_channel/ChannelsSection/ChannelsSection'
+import { OperatorsContextProvider } from '@/providers/assets/assets.provider'
+import { ConfirmationModalProvider } from '@/providers/confirmationModal'
+import { OverlayManagerProvider } from '@/providers/overlayManager'
+
+export default {
+  title: 'Channel/ChannelsSection',
+  component: ChannelsSection,
+  decorators: [
+    (Story) => {
+      const apolloClient = new ApolloClient({ cache: new InMemoryCache() })
+      const queryClient = new QueryClient({
+        defaultOptions: {
+          queries: {
+            refetchOnWindowFocus: false,
+          },
+        },
+      })
+      return (
+        <BrowserRouter>
+          <ApolloProvider client={apolloClient}>
+            <QueryClientProvider client={queryClient}>
+              <OverlayManagerProvider>
+                <OperatorsContextProvider>
+                  <ConfirmationModalProvider>
+                    <Story />
+                  </ConfirmationModalProvider>
+                </OperatorsContextProvider>
+              </OverlayManagerProvider>
+            </QueryClientProvider>
+          </ApolloProvider>
+        </BrowserRouter>
+      )
+    },
+  ],
+} as Meta
+
+export const Default: StoryFn = () => {
+  return <ChannelsSection />
+}

+ 69 - 0
packages/atlas/src/components/_channel/ChannelsSection/ChannelsSection.tsx

@@ -0,0 +1,69 @@
+import { useState } from 'react'
+
+import { useBasicChannelsConnection } from '@/api/hooks/channelsConnection'
+import { ChannelOrderByInput } from '@/api/queries/__generated__/baseTypes.generated'
+import { Section } from '@/components/Section/Section'
+import { ChannelCard } from '@/components/_channel/ChannelCard'
+
+export const ChannelsSection = () => {
+  const [sortBy, setSortBy] = useState<string>('Most followed')
+  const {
+    edges: channels,
+    pageInfo,
+    loading,
+    fetchMore,
+  } = useBasicChannelsConnection({
+    orderBy:
+      sortBy === 'Newest'
+        ? ChannelOrderByInput.CreatedAtDesc
+        : sortBy === 'Oldest'
+        ? ChannelOrderByInput.CreatedAtAsc
+        : ChannelOrderByInput.FollowsNumDesc,
+    first: 10,
+  })
+  const [isLoading, setIsLoading] = useState(false)
+
+  if (!channels || (!channels?.length && !(loading || isLoading))) {
+    return null
+  }
+
+  return (
+    <Section
+      headerProps={{
+        start: {
+          title: 'Channels',
+          type: 'title',
+        },
+        sort: {
+          type: 'toggle-button',
+          toggleButtonOptionTypeProps: {
+            type: 'options',
+            options: ['Newest', 'Oldest', 'Most followed'],
+            value: sortBy,
+            onChange: setSortBy,
+          },
+        },
+      }}
+      contentProps={{
+        type: 'grid',
+        minChildrenWidth: 200,
+        children: channels.map(({ node }) => <ChannelCard key={node.id} channel={node} />),
+      }}
+      footerProps={{
+        type: 'load',
+        label: 'Load more channels',
+        reachedEnd: !pageInfo?.hasNextPage ?? true,
+        fetchMore: async () => {
+          setIsLoading(true)
+          await fetchMore({
+            variables: {
+              after: pageInfo?.endCursor,
+            },
+          }).finally(() => {
+            setIsLoading(false)
+          })
+        },
+      }}
+    />
+  )
+}

+ 1 - 0
packages/atlas/src/components/_channel/ChannelsSection/index.ts

@@ -0,0 +1 @@
+export * from './ChannelsSection'

+ 6 - 0
packages/atlas/src/components/_inputs/ToggleButtonGroup/ToggleButtonGroup.styles.ts

@@ -42,6 +42,12 @@ export const OptionWrapper = styled.div<MaskProps>`
   }
 `
 
+export const ToggleButton = styled(Button)`
+  span {
+    white-space: nowrap;
+  }
+`
+
 export const Label = styled(Text)`
   padding: ${sizes(2)};
   align-self: center;

+ 3 - 3
packages/atlas/src/components/_inputs/ToggleButtonGroup/ToggleButtonGroup.tsx

@@ -2,7 +2,6 @@ import { useRef } from 'react'
 
 import { SvgActionChevronL, SvgActionChevronR } from '@/assets/icons'
 import { FilterButton, FilterButtonProps } from '@/components/FilterButton'
-import { Button } from '@/components/_buttons/Button'
 import { useHorizonthalFade } from '@/hooks/useHorizonthalFade'
 
 import {
@@ -13,6 +12,7 @@ import {
   ContentWrapper,
   Label,
   OptionWrapper,
+  ToggleButton,
 } from './ToggleButtonGroup.styles'
 
 type SharedToggleButtonProps = {
@@ -63,7 +63,7 @@ export const ToggleButtonGroup = <T extends string = string>(props: ToggleButton
         <OptionWrapper onMouseDown={handleMouseDown} ref={optionWrapperRef} visibleShadows={visibleShadows}>
           {type === 'options' &&
             props.options.map((option) => (
-              <Button
+              <ToggleButton
                 key={option}
                 fullWidth
                 variant={option !== props.value ? 'tertiary' : 'secondary'}
@@ -71,7 +71,7 @@ export const ToggleButtonGroup = <T extends string = string>(props: ToggleButton
                 size="small"
               >
                 {option}
-              </Button>
+              </ToggleButton>
             ))}
           {type === 'filter' &&
             props.filters.map((filterButtonProps, idx) => <FilterButton key={idx} {...filterButtonProps} />)}

+ 0 - 7
packages/atlas/src/components/_navigation/SidenavViewer/SidenavViewer.tsx

@@ -3,7 +3,6 @@ import { FC, useState } from 'react'
 import {
   SvgActionMember,
   SvgActionNewTab,
-  SvgSidebarChannels,
   SvgSidebarExplore,
   SvgSidebarHome,
   SvgSidebarMarketplace,
@@ -57,12 +56,6 @@ export const viewerNavItems = [
     to: absoluteRoutes.viewer.discover(),
     bottomNav: false,
   },
-  {
-    icon: <SvgSidebarChannels />,
-    name: 'Channels',
-    to: absoluteRoutes.viewer.channels(),
-    bottomNav: true,
-  },
   ...(atlasConfig.features.ypp.googleConsoleClientId
     ? [
         {

+ 1 - 1
packages/atlas/src/components/_templates/VideoContentTemplate.tsx

@@ -28,7 +28,7 @@ export const VideoContentTemplate: FC<VideoContentTemplateProps> = ({ children,
         </Text>
       )}
       {children}
-      {cta && <CallToActionWrapper>{ctaContent}</CallToActionWrapper>}
+      {cta && <CallToActionWrapper itemsCount={cta.length}>{ctaContent}</CallToActionWrapper>}
     </StyledViewWrapper>
   )
 }

+ 0 - 16
packages/atlas/src/views/global/YppLandingView/YppFooter.styles.ts

@@ -39,19 +39,3 @@ export const StyledButton = styled(Button)`
   margin-top: ${sizes(8)};
   background-color: ${cVar('colorCoreBaseBlack')};
 `
-
-export const CtaCardRow = styled.div<{ itemsCount: number }>`
-  display: grid;
-  gap: ${sizes(4)};
-  padding-bottom: ${sizes(16)};
-  justify-items: center;
-
-  ${media.md} {
-    margin: 0 auto;
-    justify-content: center;
-    grid-template-columns: repeat(auto-fit, minmax(219px, 1fr));
-    max-width: ${({ itemsCount }) => `calc(${itemsCount - 1} * ${sizes(6)} + 419px * ${itemsCount}) `};
-    gap: ${sizes(6)};
-    padding-bottom: ${sizes(24)};
-  }
-`

+ 4 - 4
packages/atlas/src/views/global/YppLandingView/YppFooter.tsx

@@ -3,12 +3,12 @@ import { FC, ReactElement } from 'react'
 import { SvgActionChevronR, SvgActionInfo, SvgActionSpeech, SvgActionTokensStack } from '@/assets/icons'
 import { GridItem, LayoutGrid } from '@/components/LayoutGrid'
 import { Text } from '@/components/Text'
-import { CallToActionButton } from '@/components/_buttons/CallToActionButton'
+import { CallToActionButton, CallToActionWrapper } from '@/components/_buttons/CallToActionButton'
 import { atlasConfig } from '@/config'
 import { YppWidgetIcons } from '@/config/configSchema'
 import { useMediaMatch } from '@/hooks/useMediaMatch'
 
-import { CtaBanner, CtaCardRow, StyledBannerText, StyledButton } from './YppFooter.styles'
+import { CtaBanner, StyledBannerText, StyledButton } from './YppFooter.styles'
 import { StyledLimitedWidthContainer } from './YppLandingView.styles'
 
 export const configYppIconMapper: Record<YppWidgetIcons, ReactElement> = {
@@ -55,7 +55,7 @@ export const YppFooter: FC<YppFooterSectionProps> = ({ onSignUpClick }) => {
         </LayoutGrid>
       </StyledLimitedWidthContainer>
       {atlasConfig.features.ypp.widgets && (
-        <CtaCardRow itemsCount={atlasConfig.features.ypp.widgets.length}>
+        <CallToActionWrapper itemsCount={atlasConfig.features.ypp.widgets.length}>
           {atlasConfig.features.ypp.widgets.map((widget) => (
             <CallToActionButton
               icon={widget.icon && configYppIconMapper[widget.icon]}
@@ -66,7 +66,7 @@ export const YppFooter: FC<YppFooterSectionProps> = ({ onSignUpClick }) => {
               to={widget.link}
             />
           ))}
-        </CtaCardRow>
+        </CallToActionWrapper>
       )}
     </>
   )

+ 2 - 0
packages/atlas/src/views/viewer/ChannelsView/ChannelsView.tsx

@@ -2,6 +2,7 @@ import { FC } from 'react'
 
 import { useTop10Channels } from '@/api/hooks/channel'
 import { ChannelGallery } from '@/components/_channel/ChannelGallery'
+import { ChannelsSection } from '@/components/_channel/ChannelsSection'
 import { ExpandableChannelsList } from '@/components/_channel/ExpandableChannelsList'
 import { DiscoverChannels } from '@/components/_content/DiscoverChannels'
 import { VideoContentTemplate } from '@/components/_templates/VideoContentTemplate'
@@ -22,6 +23,7 @@ export const ChannelsView: FC = () => {
         {!error ? <ChannelGallery hasRanking channels={channels} loading={loading} title="Top 10 channels" /> : null}
         <DiscoverChannels />
         <ExpandableChannelsList queryType="regular" title="Channels in your language" languageSelector />
+        <ChannelsSection />
       </VideoContentTemplate>
     </>
   )

+ 1 - 1
packages/atlas/src/views/viewer/HomeView.tsx

@@ -53,7 +53,7 @@ export const HomeView: FC = () => {
   }
 
   return (
-    <VideoContentTemplate cta={['popular', 'new', 'channels']}>
+    <VideoContentTemplate cta={['popular', 'new']}>
       {headTags}
       <VideoHero videoHeroData={videoHero} withMuteButton loading={loading} />
       <Container className={transitions.names.slide}>

+ 1 - 1
packages/atlas/src/views/viewer/NewView/NewView.tsx

@@ -10,7 +10,7 @@ export const NewView: FC = () => {
   const headTags = useHeadTags('New & Noteworthy')
 
   return (
-    <VideoContentTemplate title="New & Noteworthy" cta={['home', 'channels', 'popular']}>
+    <VideoContentTemplate title="New & Noteworthy" cta={['home', 'popular']}>
       {headTags}
       <InfiniteVideoGrid title="Recently uploaded" onDemand />
       <ExpandableChannelsList

+ 1 - 1
packages/atlas/src/views/viewer/PopularView/PopularView.tsx

@@ -10,7 +10,7 @@ import { absoluteRoutes } from '@/config/routes'
 import { useHeadTags } from '@/hooks/useHeadTags'
 import { CtaData } from '@/types/cta'
 
-const CTA: CtaData[] = ['new', 'home', 'channels']
+const CTA: CtaData[] = ['new', 'home']
 const ADDITIONAL_LINK = { name: 'Browse channels', url: absoluteRoutes.viewer.channels() }
 
 export const PopularView: FC = () => {

+ 1 - 1
packages/atlas/src/views/viewer/VideoView/VideoView.tsx

@@ -434,7 +434,7 @@ export const VideoView: FC = () => {
             {sideItems}
           </LayoutGrid>
         )}
-        <StyledCallToActionWrapper>
+        <StyledCallToActionWrapper itemsCount={3}>
           {['popular', 'new', 'discover'].map((item, idx) => (
             <CallToActionButton key={`cta-${idx}`} {...CTA_MAP[item]} />
           ))}