Forráskód Böngészése

Add CTA buttons to all video content pages (#1136)

Rafał Pawłow 3 éve
szülő
commit
108c9eeea9

+ 2 - 13
src/shared/components/ButtonBase/ButtonBase.tsx

@@ -1,6 +1,7 @@
 import { To } from 'history'
 import React from 'react'
-import { Link } from 'react-router-dom'
+
+import { getLinkPropsFromTo } from '@/utils/button'
 
 import { BorderWrapper, ButtonBaseStyleProps, StyledButtonBase } from './ButtonBase.style'
 
@@ -15,18 +16,6 @@ export type ButtonBaseProps = {
   className?: string
 } & Partial<Pick<ButtonBaseStyleProps, 'size' | 'variant' | 'fullWidth'>>
 
-const getLinkPropsFromTo = (to?: To) => {
-  if (!to) {
-    return {}
-  }
-
-  if (typeof to === 'string' && to.includes('http')) {
-    return { as: 'a', href: to, rel: 'noopener noreferrer', target: '_blank' } as const
-  }
-
-  return { as: Link, to: to }
-}
-
 export const ButtonBase = React.forwardRef<HTMLButtonElement, ButtonBaseProps>(
   (
     {

+ 30 - 0
src/shared/components/CallToActionButton/CallToActionButton.stories.tsx

@@ -0,0 +1,30 @@
+import { Meta, Story } from '@storybook/react'
+import React from 'react'
+
+import { SvgNavPopular } from '@/shared/icons'
+
+import { CallToActionButton, CallToActionButtonProps, CallToActionWrapper } from '.'
+
+export default {
+  title: 'Shared/C/CallToActionButton',
+  component: CallToActionButton,
+  argTypes: {
+    colorVariant: {
+      control: { type: 'select', options: ['blue', 'green', 'red', 'yellow'] },
+      defaultValue: 'blue',
+    },
+  },
+} as Meta
+
+const Template: Story<CallToActionButtonProps> = (args) => {
+  return (
+    <CallToActionWrapper>
+      <CallToActionButton {...args} />
+    </CallToActionWrapper>
+  )
+}
+export const Default = Template.bind({})
+Default.args = {
+  label: 'Call To Action Button',
+  icon: <SvgNavPopular />,
+}

+ 75 - 0
src/shared/components/CallToActionButton/CallToActionButton.style.ts

@@ -0,0 +1,75 @@
+import isPropValid from '@emotion/is-prop-valid'
+import styled from '@emotion/styled'
+
+import { colors, media, sizes, transitions } from '@/shared/theme'
+
+import { CallToActionButtonProps } from '.'
+
+const mappedColors = {
+  blue: colors.blue[500],
+  red: colors.secondary.alert[100],
+  yellow: colors.secondary.warning[100],
+  green: colors.secondary.success[100],
+}
+
+export const CallToActionWrapper = styled.div`
+  margin-top: ${sizes(19)};
+
+  ${media.medium} {
+    display: grid;
+    grid-template-columns: auto auto auto;
+    grid-column-gap: ${sizes(6)};
+  }
+`
+
+export const IconWrapper = styled.div`
+  margin-bottom: ${sizes(5)};
+`
+
+export const BodyWrapper = styled.div`
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+`
+
+export const ContentWrapper = styled.div`
+  padding: ${sizes(8)};
+  background-color: ${colors.gray[800]};
+  transition: all ${transitions.timings.regular} ${transitions.easing};
+`
+type StyledContainerProps = Omit<CallToActionButtonProps, 'label'>
+
+export const StyledContainer = styled('button', { shouldForwardProp: isPropValid })<StyledContainerProps>`
+  display: block;
+  width: 100%;
+  align-items: center;
+  cursor: pointer;
+  border: 0;
+  color: ${colors.white};
+  text-decoration: none;
+  background-color: transparent;
+
+  &:not(:last-child) {
+    margin-bottom: ${sizes(4)};
+  }
+
+  ${media.medium} {
+    &:not(:last-child) {
+      margin-bottom: 0;
+    }
+  }
+
+  :hover {
+    ${ContentWrapper} {
+      transform: translate(-${sizes(2)}, -${sizes(2)});
+      box-shadow: ${({ colorVariant = 'blue' }) => `${sizes(2)} ${sizes(2)} 0 ${mappedColors[colorVariant]}`};
+    }
+  }
+
+  ${IconWrapper} {
+    path,
+    circle {
+      stroke: ${({ colorVariant = 'blue' }) => mappedColors[colorVariant]};
+    }
+  }
+`

+ 39 - 0
src/shared/components/CallToActionButton/CallToActionButton.tsx

@@ -0,0 +1,39 @@
+import { To } from 'history'
+import React, { FC, MouseEvent, ReactNode } from 'react'
+
+import { SvgGlyphChevronRight } from '@/shared/icons'
+import { getLinkPropsFromTo } from '@/utils/button'
+
+import { BodyWrapper, ContentWrapper, IconWrapper, StyledContainer } from './CallToActionButton.style'
+
+export type ColorVariants = 'red' | 'green' | 'yellow' | 'blue'
+
+export type CallToActionButtonProps = {
+  to?: To
+  onClick?: (event: MouseEvent<HTMLButtonElement>) => void
+  icon?: ReactNode
+  colorVariant?: ColorVariants
+  label: string
+}
+
+export const CallToActionButton: FC<CallToActionButtonProps> = ({
+  to,
+  icon,
+  onClick,
+  colorVariant = 'blue',
+  label,
+}) => {
+  const linkProps = getLinkPropsFromTo(to)
+
+  return (
+    <StyledContainer {...linkProps} onClick={onClick} colorVariant={colorVariant}>
+      <ContentWrapper>
+        <IconWrapper>{icon}</IconWrapper>
+        <BodyWrapper>
+          {label}
+          <SvgGlyphChevronRight />
+        </BodyWrapper>
+      </ContentWrapper>
+    </StyledContainer>
+  )
+}

+ 3 - 0
src/shared/components/CallToActionButton/index.ts

@@ -0,0 +1,3 @@
+export * from './CallToActionButton'
+export { CallToActionWrapper } from './CallToActionButton.style'
+export type { CallToActionButtonProps } from './CallToActionButton'

+ 2 - 2
src/shared/components/ChannelCardBase/ChannelCardBase.tsx

@@ -4,7 +4,7 @@ import { CSSTransition, SwitchTransition } from 'react-transition-group'
 import { useFollowChannel, useUnfollowChannel } from '@/api/hooks'
 import { usePersonalDataStore } from '@/providers'
 import { transitions } from '@/shared/theme'
-import { Logger } from '@/utils/logger'
+import { SentryLogger } from '@/utils/logs'
 
 import {
   Anchor,
@@ -82,7 +82,7 @@ export const ChannelCardBase: React.FC<ChannelCardBaseProps> = ({
           setFollowing(true)
         }
       } catch (error) {
-        Logger.warn('Failed to update Channel following', { error })
+        SentryLogger.error('Failed to update channel following', 'ChannelView', error, { channel: { id: channelId } })
       }
     }
   }

+ 1 - 0
src/shared/components/index.ts

@@ -46,4 +46,5 @@ export * from './AnimatedError'
 export * from './EmptyFallback'
 export * from './LayoutGrid/LayoutGrid'
 export * from './LoadMoreButton'
+export * from './CallToActionButton'
 export * from './GridHeading'

+ 14 - 0
src/utils/button.ts

@@ -0,0 +1,14 @@
+import { To } from 'history'
+import { Link } from 'react-router-dom'
+
+export const getLinkPropsFromTo = (to?: To) => {
+  if (!to) {
+    return {}
+  }
+
+  if (typeof to === 'string' && to.includes('http')) {
+    return { as: 'a', href: to, rel: 'noopener noreferrer', target: '_blank' } as const
+  }
+
+  return { as: Link, to: to }
+}

+ 23 - 1
src/views/viewer/ChannelsView/ChannelsView.tsx

@@ -2,7 +2,9 @@ import styled from '@emotion/styled'
 import React from 'react'
 
 import { InfiniteChannelWithVideosGrid, ViewWrapper } from '@/components'
-import { Text } from '@/shared/components'
+import { absoluteRoutes } from '@/config/routes'
+import { CallToActionButton, CallToActionWrapper, Text } from '@/shared/components'
+import { SvgNavHome, SvgNavNew, SvgNavPopular } from '@/shared/icons'
 import { sizes } from '@/shared/theme'
 
 export const ChannelsView = () => {
@@ -10,6 +12,26 @@ export const ChannelsView = () => {
     <StyledViewWrapper>
       <Header variant="h2">Browse channels</Header>
       <InfiniteChannelWithVideosGrid title="Channels in your language:" languageSelector onDemand />
+      <CallToActionWrapper>
+        <CallToActionButton
+          label="Popular on Joystream"
+          to={absoluteRoutes.viewer.popular()}
+          colorVariant="red"
+          icon={<SvgNavPopular />}
+        />
+        <CallToActionButton
+          label="New & Noteworthy"
+          to={absoluteRoutes.viewer.new()}
+          colorVariant="green"
+          icon={<SvgNavNew />}
+        />
+        <CallToActionButton
+          label="Home"
+          to={absoluteRoutes.viewer.index()}
+          colorVariant="yellow"
+          icon={<SvgNavHome />}
+        />
+      </CallToActionWrapper>
     </StyledViewWrapper>
   )
 }

+ 23 - 0
src/views/viewer/HomeView.tsx

@@ -10,7 +10,10 @@ import {
   VideoHero,
   ViewErrorFallback,
 } from '@/components'
+import { absoluteRoutes } from '@/config/routes'
 import { usePersonalDataStore } from '@/providers'
+import { CallToActionButton, CallToActionWrapper } from '@/shared/components'
+import { SvgNavChannels, SvgNavNew, SvgNavPopular } from '@/shared/icons'
 import { sizes, transitions } from '@/shared/theme'
 import { SentryLogger } from '@/utils/logs'
 
@@ -64,6 +67,26 @@ export const HomeView: React.FC = () => {
         <OfficialJoystreamUpdate />
         <TopTenThisWeek />
         <StyledInfiniteVideoGrid title="All content" onDemand />
+        <CallToActionWrapper>
+          <CallToActionButton
+            label="Popular on Joystream"
+            to={absoluteRoutes.viewer.popular()}
+            colorVariant="red"
+            icon={<SvgNavPopular />}
+          />
+          <CallToActionButton
+            label="New & Noteworthy"
+            to={absoluteRoutes.viewer.new()}
+            colorVariant="green"
+            icon={<SvgNavNew />}
+          />
+          <CallToActionButton
+            label="Browse channels"
+            to={absoluteRoutes.viewer.channels()}
+            colorVariant="blue"
+            icon={<SvgNavChannels />}
+          />
+        </CallToActionWrapper>
       </Container>
     </LimitedWidthContainer>
   )

+ 24 - 8
src/views/viewer/NewView/NewView.tsx

@@ -1,17 +1,33 @@
 import styled from '@emotion/styled'
-import React from 'react'
+import React, { FC } from 'react'
 
 import { ViewWrapper } from '@/components'
+import { absoluteRoutes } from '@/config/routes'
+import { CallToActionButton, CallToActionWrapper } from '@/shared/components'
 import { Text } from '@/shared/components'
+import { SvgNavChannels, SvgNavHome, SvgNavPopular } from '@/shared/icons'
 import { sizes } from '@/shared/theme'
 
-export const NewView = () => {
-  return (
-    <ViewWrapper>
-      <Header variant="h2">New & Noteworthy</Header>
-    </ViewWrapper>
-  )
-}
+export const NewView: FC = () => (
+  <ViewWrapper>
+    <Header variant="h2">New & Noteworthy</Header>
+    <CallToActionWrapper>
+      <CallToActionButton label="Home" to={absoluteRoutes.viewer.index()} colorVariant="yellow" icon={<SvgNavHome />} />
+      <CallToActionButton
+        label="Browse channels"
+        to={absoluteRoutes.viewer.channels()}
+        colorVariant="blue"
+        icon={<SvgNavChannels />}
+      />
+      <CallToActionButton
+        label="Popular on Joystream"
+        to={absoluteRoutes.viewer.popular()}
+        colorVariant="red"
+        icon={<SvgNavPopular />}
+      />
+    </CallToActionWrapper>
+  </ViewWrapper>
+)
 
 const Header = styled(Text)`
   margin-top: ${sizes(16)};

+ 24 - 8
src/views/viewer/PopularView/PopularView.tsx

@@ -1,17 +1,33 @@
 import styled from '@emotion/styled'
-import React from 'react'
+import React, { FC } from 'react'
 
 import { ViewWrapper } from '@/components'
+import { absoluteRoutes } from '@/config/routes'
+import { CallToActionButton, CallToActionWrapper } from '@/shared/components'
 import { Text } from '@/shared/components'
+import { SvgNavChannels, SvgNavHome, SvgNavNew } from '@/shared/icons'
 import { sizes } from '@/shared/theme'
 
-export const PopularView = () => {
-  return (
-    <ViewWrapper>
-      <Header variant="h2">Popular</Header>
-    </ViewWrapper>
-  )
-}
+export const PopularView: FC = () => (
+  <ViewWrapper>
+    <Header variant="h2">Popular</Header>
+    <CallToActionWrapper>
+      <CallToActionButton
+        label="New & Noteworthy"
+        to={absoluteRoutes.viewer.new()}
+        colorVariant="green"
+        icon={<SvgNavNew />}
+      />
+      <CallToActionButton label="Home" to={absoluteRoutes.viewer.index()} colorVariant="yellow" icon={<SvgNavHome />} />
+      <CallToActionButton
+        label="Browse channels"
+        to={absoluteRoutes.viewer.channels()}
+        colorVariant="blue"
+        icon={<SvgNavChannels />}
+      />
+    </CallToActionWrapper>
+  </ViewWrapper>
+)
 
 const Header = styled(Text)`
   margin-top: ${sizes(16)};