Browse Source

Add Channel Screen

Francesco Baccetti 4 years ago
parent
commit
781a8709f6
27 changed files with 319 additions and 109 deletions
  1. 12 0
      packages/app/__generated__/globalTypes.ts
  2. 1 0
      packages/app/src/api/queries/__generated__/ChannelFields.ts
  3. 25 0
      packages/app/src/api/queries/__generated__/GetChannel.ts
  4. 66 0
      packages/app/src/api/queries/__generated__/GetFullChannel.ts
  5. 1 0
      packages/app/src/api/queries/__generated__/GetNewestChannels.ts
  6. 1 0
      packages/app/src/api/queries/__generated__/Search.ts
  7. 24 0
      packages/app/src/api/queries/__generated__/channel.ts
  8. 23 0
      packages/app/src/api/queries/channels.ts
  9. 5 0
      packages/app/src/components/ChannelGallery.tsx
  10. 2 1
      packages/app/src/components/LayoutWithRouting.tsx
  11. 4 0
      packages/app/src/components/VideoGallery.tsx
  12. 1 0
      packages/app/src/config/routes.ts
  13. 2 1
      packages/app/src/mocking/data/mockChannels.ts
  14. 15 0
      packages/app/src/mocking/data/mockImages.ts
  15. 0 16
      packages/app/src/shared/__tests__/Grid.test.tsx
  16. 10 1
      packages/app/src/shared/components/ChannelPreview/ChannelPreview.tsx
  17. 3 0
      packages/app/src/shared/components/ChannelPreview/ChannelPreviewBase.style.tsx
  18. 9 1
      packages/app/src/shared/components/ChannelPreview/ChannelPreviewBase.tsx
  19. 0 20
      packages/app/src/shared/components/Grid/Grid.style.ts
  20. 0 20
      packages/app/src/shared/components/Grid/Grid.tsx
  21. 0 2
      packages/app/src/shared/components/Grid/index.ts
  22. 0 1
      packages/app/src/shared/components/index.ts
  23. 0 45
      packages/app/src/shared/stories/X-Grid.stories.tsx
  24. 46 0
      packages/app/src/views/ChannelView/ChannelView.style.tsx
  25. 66 0
      packages/app/src/views/ChannelView/ChannelView.tsx
  26. 1 0
      packages/app/src/views/ChannelView/index.ts
  27. 2 1
      packages/app/src/views/index.ts

+ 12 - 0
packages/app/__generated__/globalTypes.ts

@@ -0,0 +1,12 @@
+/* tslint:disable */
+/* eslint-disable */
+// @generated
+// This file was automatically generated and should not be edited.
+
+//==============================================================
+// START Enums and Input Objects
+//==============================================================
+
+//==============================================================
+// END Enums and Input Objects
+//==============================================================

+ 1 - 0
packages/app/src/api/queries/__generated__/ChannelFields.ts

@@ -12,5 +12,6 @@ export interface ChannelFields {
   id: string;
   handle: string;
   avatarPhotoURL: string;
+  coverPhotoURL: string;
   totalViews: number;
 }

+ 25 - 0
packages/app/src/api/queries/__generated__/GetChannel.ts

@@ -0,0 +1,25 @@
+/* tslint:disable */
+/* eslint-disable */
+// @generated
+// This file was automatically generated and should not be edited.
+
+// ====================================================
+// GraphQL query operation: GetChannel
+// ====================================================
+
+export interface GetChannel_channel {
+  __typename: "Channel";
+  id: string;
+  handle: string;
+  avatarPhotoURL: string;
+  coverPhotoURL: string;
+  totalViews: number;
+}
+
+export interface GetChannel {
+  channel: GetChannel_channel | null;
+}
+
+export interface GetChannelVariables {
+  id: string;
+}

+ 66 - 0
packages/app/src/api/queries/__generated__/GetFullChannel.ts

@@ -0,0 +1,66 @@
+/* tslint:disable */
+/* eslint-disable */
+// @generated
+// This file was automatically generated and should not be edited.
+
+// ====================================================
+// GraphQL query operation: GetFullChannel
+// ====================================================
+
+export interface GetFullChannel_channel_videos_media_location_HTTPVideoMediaLocation {
+  __typename: "HTTPVideoMediaLocation";
+  host: string;
+  port: number | null;
+}
+
+export interface GetFullChannel_channel_videos_media_location_JoystreamVideoMediaLocation {
+  __typename: "JoystreamVideoMediaLocation";
+  dataObjectID: string;
+}
+
+export type GetFullChannel_channel_videos_media_location = GetFullChannel_channel_videos_media_location_HTTPVideoMediaLocation | GetFullChannel_channel_videos_media_location_JoystreamVideoMediaLocation;
+
+export interface GetFullChannel_channel_videos_media {
+  __typename: "VideoMedia";
+  pixelHeight: number;
+  pixelWidth: number;
+  location: GetFullChannel_channel_videos_media_location;
+}
+
+export interface GetFullChannel_channel_videos_channel {
+  __typename: "Channel";
+  id: string;
+  avatarPhotoURL: string;
+  handle: string;
+}
+
+export interface GetFullChannel_channel_videos {
+  __typename: "Video";
+  id: string;
+  title: string;
+  description: string;
+  views: number;
+  duration: number;
+  thumbnailURL: string;
+  publishedOnJoystreamAt: GQLDate;
+  media: GetFullChannel_channel_videos_media;
+  channel: GetFullChannel_channel_videos_channel;
+}
+
+export interface GetFullChannel_channel {
+  __typename: "Channel";
+  id: string;
+  handle: string;
+  avatarPhotoURL: string;
+  coverPhotoURL: string;
+  totalViews: number;
+  videos: GetFullChannel_channel_videos[] | null;
+}
+
+export interface GetFullChannel {
+  channel: GetFullChannel_channel | null;
+}
+
+export interface GetFullChannelVariables {
+  id: string;
+}

+ 1 - 0
packages/app/src/api/queries/__generated__/GetNewestChannels.ts

@@ -12,6 +12,7 @@ export interface GetNewestChannels_channels {
   id: string;
   handle: string;
   avatarPhotoURL: string;
+  coverPhotoURL: string;
   totalViews: number;
 }
 

+ 1 - 0
packages/app/src/api/queries/__generated__/Search.ts

@@ -52,6 +52,7 @@ export interface Search_search_item_Channel {
   id: string;
   handle: string;
   avatarPhotoURL: string;
+  coverPhotoURL: string;
   totalViews: number;
 }
 

+ 24 - 0
packages/app/src/api/queries/__generated__/channel.ts

@@ -0,0 +1,24 @@
+/* tslint:disable */
+/* eslint-disable */
+// @generated
+// This file was automatically generated and should not be edited.
+
+// ====================================================
+// GraphQL query operation: channel
+// ====================================================
+
+export interface channel_channel {
+  __typename: "Channel";
+  id: string;
+  handle: string;
+  avatarPhotoURL: string;
+  totalViews: number;
+}
+
+export interface channel {
+  channel: channel_channel | null;
+}
+
+export interface channelVariables {
+  id: string;
+}

+ 23 - 0
packages/app/src/api/queries/channels.ts

@@ -1,10 +1,12 @@
 import gql from 'graphql-tag'
+import { videoFieldsFragment } from './videos'
 
 export const channelFieldsFragment = gql`
   fragment ChannelFields on Channel {
     id
     handle
     avatarPhotoURL
+    coverPhotoURL
     totalViews
   }
 `
@@ -18,3 +20,24 @@ export const GET_NEWEST_CHANNELS = gql`
   }
   ${channelFieldsFragment}
 `
+
+export const GET_CHANNEL = gql`
+  query GetChannel($id: ID!) {
+    channel(id: $id) {
+      ...ChannelFields
+    }
+  }
+  ${channelFieldsFragment}
+`
+export const GET_FULL_CHANNEL = gql`
+  query GetFullChannel($id: ID!) {
+    channel(id: $id) {
+      ...ChannelFields
+      videos {
+        ...VideoFields
+      }
+    }
+  }
+  ${channelFieldsFragment}
+  ${videoFieldsFragment}
+`

+ 5 - 0
packages/app/src/components/ChannelGallery.tsx

@@ -1,7 +1,10 @@
 import React from 'react'
 import styled from '@emotion/styled'
+import { navigate } from '@reach/router'
+
 import { ChannelPreview, ChannelPreviewBase, Gallery } from '@/shared/components'
 import { ChannelFields } from '@/api/queries/__generated__/ChannelFields'
+import routes from '@/config/routes'
 
 type ChannelGalleryProps = {
   title: string
@@ -27,6 +30,8 @@ const ChannelGallery: React.FC<ChannelGalleryProps> = ({ title, action, channels
               avatarURL={channel.avatarPhotoURL}
               views={channel.totalViews}
               key={channel.id}
+              onClick={() => navigate(routes.channel(channel.id))}
+              animated
             />
           ))}
     </Gallery>

+ 2 - 1
packages/app/src/components/LayoutWithRouting.tsx

@@ -1,7 +1,7 @@
 import React from 'react'
 import { GlobalStyle } from '@/shared/components'
 import { Navbar } from '@/components'
-import { HomeView, VideoView, SearchView } from '@/views'
+import { HomeView, VideoView, SearchView, ChannelView } from '@/views'
 import routes from '@/config/routes'
 import { Router } from '@reach/router'
 
@@ -15,6 +15,7 @@ const LayoutWithRouting: React.FC = () => (
       <HomeView default />
       <VideoView path={routes.video()} />
       <SearchView path={routes.search()} />
+      <ChannelView path={routes.channel()} />
     </Router>
   </main>
 )

+ 4 - 0
packages/app/src/components/VideoGallery.tsx

@@ -43,6 +43,9 @@ const VideoGallery: React.FC<VideoGalleryProps> = ({ title, action, videos, load
   const handleVideoClick = (id: string) => {
     navigate(routes.video(id))
   }
+  const handleChannelClick = (id: string) => {
+    navigate(routes.channel(id))
+  }
 
   return (
     <Gallery
@@ -66,6 +69,7 @@ const VideoGallery: React.FC<VideoGalleryProps> = ({ title, action, videos, load
               duration={video.duration}
               posterURL={video.thumbnailURL}
               onClick={() => handleVideoClick(video.id)}
+              onChannelClick={() => handleChannelClick(video.channel.id)}
               imgRef={idx === 0 ? imgRef : null}
               key={video.id}
             />

+ 1 - 0
packages/app/src/config/routes.ts

@@ -1,4 +1,5 @@
 export default {
   video: (id = ':id') => `/video/${id}`,
   search: (searchStr = ':search') => `/search/${searchStr}`,
+  channel: (id = ':id') => `/channel/${id}`,
 }

+ 2 - 1
packages/app/src/mocking/data/mockChannels.ts

@@ -1,5 +1,5 @@
 import { shuffle } from 'lodash'
-import { channelSources } from './mockImages'
+import { channelSources, coverSources } from './mockImages'
 import { ChannelFields } from '@/api/queries/__generated__/ChannelFields'
 
 const rawChannels = [
@@ -62,6 +62,7 @@ const mockChannels: RawChannel[] = shuffledRawChannels.map((rawChannel, idx) =>
   ...rawChannel,
   __typename: 'Channel',
   avatarPhotoURL: channelSources[idx % channelSources.length],
+  coverPhotoURL: coverSources[idx % coverSources.length],
 }))
 
 export default mockChannels

+ 15 - 0
packages/app/src/mocking/data/mockImages.ts

@@ -19,3 +19,18 @@ export const posterSources = [
   'https://source.unsplash.com/collection/1198157/640x380',
   'https://source.unsplash.com/collection/207682/640x380',
 ]
+
+export const coverSources = [
+  'https://source.unsplash.com/collection/1538150/1600x900',
+  'https://source.unsplash.com/collection/1424240/1600x900',
+  'https://source.unsplash.com/collection/2569191/1600x900',
+  'https://source.unsplash.com/collection/357786/1600x900',
+  'https://source.unsplash.com/collection/1360971/1600x900',
+  'https://source.unsplash.com/collection/935527/1600x900',
+  'https://source.unsplash.com/collection/1120118/1600x900',
+  'https://source.unsplash.com/collection/369/1600x900',
+  'https://source.unsplash.com/collection/827737/1600x900',
+  'https://source.unsplash.com/collection/782142/1600x900',
+  'https://source.unsplash.com/collection/1198157/1600x900',
+  'https://source.unsplash.com/collection/207682/1600x900',
+]

+ 0 - 16
packages/app/src/shared/__tests__/Grid.test.tsx

@@ -1,16 +0,0 @@
-import React from 'react'
-import { shallow } from 'enzyme'
-import Grid from '@/shared/components/Grid'
-describe('Grid component', () => {
-  it('Should render Grid correctly', () => {
-    expect(
-      shallow(
-        <Grid>
-          <div>Grid Elemen 1</div>
-          <div>Grid Elemen 2</div>
-          <div>Grid Elemen 3</div>
-        </Grid>
-      )
-    ).toBeDefined()
-  })
-})

+ 10 - 1
packages/app/src/shared/components/ChannelPreview/ChannelPreview.tsx

@@ -11,9 +11,17 @@ type ChannelPreviewProps = {
   avatarURL?: string
   className?: string
   animated?: boolean
+  onClick?: (e: React.MouseEvent<HTMLElement>) => void
 }
 
-const ChannelPreview: React.FC<ChannelPreviewProps> = ({ name, avatarURL, views, className, animated = false }) => {
+const ChannelPreview: React.FC<ChannelPreviewProps> = ({
+  name,
+  avatarURL,
+  views,
+  className,
+  animated = false,
+  onClick,
+}) => {
   const avatarNode = <StyledAvatar img={avatarURL} />
   const nameNode = <NameHeader>{name}</NameHeader>
   const metaNode = <MetaSpan>{formatNumberShort(views)} views</MetaSpan>
@@ -25,6 +33,7 @@ const ChannelPreview: React.FC<ChannelPreviewProps> = ({ name, avatarURL, views,
       nameNode={nameNode}
       metaNode={metaNode}
       animated={animated}
+      onClick={onClick}
     />
   )
 }

+ 3 - 0
packages/app/src/shared/components/ChannelPreview/ChannelPreviewBase.style.tsx

@@ -8,6 +8,9 @@ export const OuterContainer = styled.article`
   width: 200px;
   height: ${`calc(186px + ${imageTopOverflow})`};
   padding-top: ${imageTopOverflow};
+  :hover {
+    cursor: ${(props) => (props.onClick ? 'pointer' : 'default')};
+  }
 `
 
 type InnerContainerProps = {

+ 9 - 1
packages/app/src/shared/components/ChannelPreview/ChannelPreviewBase.tsx

@@ -9,6 +9,7 @@ type ChannelPreviewBaseProps = {
   metaNode?: React.ReactNode
   className?: string
   animated?: boolean
+  onClick?: (e: React.MouseEvent<HTMLElement>) => void
 }
 
 const ChannelPreviewBase: React.FC<ChannelPreviewBaseProps> = ({
@@ -17,13 +18,20 @@ const ChannelPreviewBase: React.FC<ChannelPreviewBaseProps> = ({
   metaNode,
   className,
   animated = false,
+  onClick,
 }) => {
   const avatarPlaceholder = <Placeholder rounded />
   const namePlaceholder = <Placeholder width="140px" height="16px" />
   const metaPlaceholder = <MetaPlaceholder width="80px" height="12px" />
 
+  const handleClick = (e: React.MouseEvent<HTMLElement>) => {
+    if (!onClick) return
+
+    onClick(e)
+  }
+
   return (
-    <OuterContainer className={className}>
+    <OuterContainer className={className} onClick={handleClick}>
       <InnerContainer animated={animated}>
         <AvatarContainer>{avatarNode || avatarPlaceholder}</AvatarContainer>
         <Info>

+ 0 - 20
packages/app/src/shared/components/Grid/Grid.style.ts

@@ -1,20 +0,0 @@
-import { makeStyles, StyleFn } from '../../utils'
-
-export type GridStyleProps = {
-  minItemWidth?: string | number
-  maxItemWidth?: string | number
-}
-const container: StyleFn = (_, { minItemWidth = '300', maxItemWidth }) => ({
-  display: 'grid',
-  gridTemplateColumns: `repeat(auto-fit, minmax(${minItemWidth}px, ${maxItemWidth ? `${maxItemWidth}px` : '1fr'}))`,
-  gap: '30px',
-})
-
-const item: StyleFn = () => ({
-  width: '100%',
-})
-
-export const useCSS = (props: GridStyleProps) => ({
-  container: makeStyles([container])(props),
-  item: makeStyles([item])(props),
-})

+ 0 - 20
packages/app/src/shared/components/Grid/Grid.tsx

@@ -1,20 +0,0 @@
-import React from 'react'
-import { GridStyleProps, useCSS } from './Grid.style'
-
-type SectionProps = {
-  items?: React.ReactNode[]
-  className?: string
-} & GridStyleProps
-
-export default function Grid({ items = [], className = '', ...styleProps }: SectionProps) {
-  const styles = useCSS(styleProps)
-  return (
-    <div css={styles.container} className={className}>
-      {items.map((item, index) => (
-        <div key={`grid-item-${index}`} css={styles.item}>
-          {item}
-        </div>
-      ))}
-    </div>
-  )
-}

+ 0 - 2
packages/app/src/shared/components/Grid/index.ts

@@ -1,2 +0,0 @@
-import Grid from './Grid'
-export default Grid

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

@@ -2,7 +2,6 @@ export { default as Avatar } from './Avatar'
 export { default as Button } from './Button'
 export { default as Carousel } from './Carousel'
 export { default as Dropdown } from './Dropdown'
-export { default as Grid } from './Grid'
 export { default as Header } from './Header'
 export { default as Link } from './Link'
 export { default as NavButton } from './NavButton'

+ 0 - 45
packages/app/src/shared/stories/X-Grid.stories.tsx

@@ -1,45 +0,0 @@
-import React from 'react'
-import { Grid } from '../components'
-
-export default {
-  title: 'Grid',
-  component: Grid,
-}
-
-function Item() {
-  return (
-    <div>
-      <img
-        src="https://27pc93zx53q14ywwgt4yq513-wpengine.netdna-ssl.com/wp-content/uploads/2016/08/video-placeholder-brain-bites.png"
-        style={{ width: '100%' }}
-      />
-      <p>Item title</p>
-    </div>
-  )
-}
-
-export const Default = () => (
-  <Grid
-    items={Array.from({ length: 12 }).map((_, idx) => (
-      <Item key={idx} />
-    ))}
-  />
-)
-
-export const WithMinItemWidth300 = () => (
-  <Grid
-    minItemWidth="300"
-    items={Array.from({ length: 12 }).map((_, idx) => (
-      <Item key={idx} />
-    ))}
-  />
-)
-
-export const WithClassName = () => (
-  <Grid
-    className="customGrid"
-    items={Array.from({ length: 12 }).map((_, idx) => (
-      <Item key={idx} />
-    ))}
-  />
-)

+ 46 - 0
packages/app/src/views/ChannelView/ChannelView.style.tsx

@@ -0,0 +1,46 @@
+import styled from '@emotion/styled'
+import { Avatar } from '@/shared/components'
+import theme from '@/shared/theme'
+
+type ChannelHeaderProps = {
+  coverPhotoURL: string
+}
+export const Header = styled.section<ChannelHeaderProps>`
+  background-image: linear-gradient(0deg, #000000 10.85%, rgba(0, 0, 0, 0) 88.35%),
+    ${(props) => `url(${props.coverPhotoURL})`};
+  background-size: cover;
+  background-position: center center;
+  background-repeat: no-repeat;
+  height: 430px;
+  padding: 0 ${theme.sizes.b8}px;
+`
+export const TitleSection = styled.div`
+  display: flex;
+  align-items: center;
+  padding-top: ${theme.sizes.b10}px;
+`
+export const Title = styled.h1`
+  font-size: ${theme.typography.sizes.h2}px;
+  font-weight: bold;
+  max-width: 320px;
+  display: inline-block;
+`
+
+export const VideoSection = styled.section`
+  padding: 0 ${theme.sizes.b8}px;
+  margin-top: -100px;
+`
+export const VideoSectionHeader = styled.h5`
+  margin: 0 0 ${theme.spacing.m};
+  font-size: ${theme.typography.sizes.h5};
+`
+
+export const VideoSectionGrid = styled.div`
+  display: grid;
+  grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
+  grid-gap: ${theme.spacing.xl};
+`
+export const StyledAvatar = styled(Avatar)`
+  max-width: 136px;
+  margin-right: ${theme.sizes.b6}px;
+`

+ 66 - 0
packages/app/src/views/ChannelView/ChannelView.tsx

@@ -0,0 +1,66 @@
+import React from 'react'
+import { RouteComponentProps, useParams, navigate } from '@reach/router'
+import { useQuery } from '@apollo/client'
+
+import routes from '@/config/routes'
+import { GET_FULL_CHANNEL } from '@/api/queries/channels'
+import { GetFullChannel, GetFullChannelVariables } from '@/api/queries/__generated__/GetFullChannel'
+import { VideoPreview } from '@/shared/components'
+
+import {
+  Header,
+  VideoSection,
+  VideoSectionHeader,
+  VideoSectionGrid,
+  Title,
+  TitleSection,
+  StyledAvatar,
+} from './ChannelView.style'
+
+const ChannelView: React.FC<RouteComponentProps> = () => {
+  const { id } = useParams()
+  const { loading, data } = useQuery<GetFullChannel, GetFullChannelVariables>(GET_FULL_CHANNEL, {
+    variables: { id },
+  })
+
+  if (loading || !data?.channel) {
+    return <p>Loading Channel...</p>
+  }
+  const videos = data?.channel?.videos || []
+
+  const handleVideoClick = (id: string) => {
+    navigate(routes.video(id))
+  }
+
+  return (
+    <div>
+      <Header coverPhotoURL={data.channel.coverPhotoURL}>
+        <TitleSection>
+          <StyledAvatar img={data.channel.avatarPhotoURL} />
+          <Title>{data.channel.handle}</Title>
+        </TitleSection>
+      </Header>
+      {videos.length > 0 && (
+        <VideoSection>
+          <VideoSectionHeader>Videos</VideoSectionHeader>
+          <VideoSectionGrid>
+            {videos.map((video, idx) => (
+              <VideoPreview
+                key={idx}
+                title={video.title}
+                channelName={video.channel.handle}
+                channelAvatarURL={video.channel.avatarPhotoURL}
+                createdAt={video.publishedOnJoystreamAt}
+                duration={video.duration}
+                views={video.views}
+                posterURL={video.thumbnailURL}
+                onClick={() => handleVideoClick(video.id)}
+              />
+            ))}
+          </VideoSectionGrid>
+        </VideoSection>
+      )}
+    </div>
+  )
+}
+export default ChannelView

+ 1 - 0
packages/app/src/views/ChannelView/index.ts

@@ -0,0 +1 @@
+export { default } from './ChannelView'

+ 2 - 1
packages/app/src/views/index.ts

@@ -1,4 +1,5 @@
 import HomeView from './HomeView'
 import VideoView from './VideoView'
 import SearchView from './SearchView'
-export { HomeView, VideoView, SearchView }
+import ChannelView from './ChannelView'
+export { HomeView, VideoView, SearchView, ChannelView }