Эх сурвалжийг харах

Merge pull request #1660 from kdembler/atlas-featured-video

add cover featured video
Bedeho Mender 4 жил өмнө
parent
commit
57380586d7

+ 136 - 0
src/components/FeaturedVideoHeader/FeaturedVideoHeader.style.ts

@@ -0,0 +1,136 @@
+import styled from '@emotion/styled'
+import { fluidRange } from 'polished'
+
+import { Avatar, Button } from '@/shared/components'
+import { breakpoints, colors, spacing, typography } from '@/shared/theme'
+import { Link } from '@reach/router'
+
+export const Container = styled.section`
+  position: relative;
+
+  // because of the fixed aspect ratio, as the viewport width grows, the media will occupy more height as well
+  // so that the media doesn't take too big of a portion of the space, we let the content overlap the media via a negative margin
+  @media screen and (min-width: ${breakpoints.small}) {
+    margin-bottom: -75px;
+  }
+  @media screen and (min-width: ${breakpoints.medium}) {
+    margin-bottom: -200px;
+  }
+  @media screen and (min-width: ${breakpoints.large}) {
+    margin-bottom: -250px;
+  }
+  @media screen and (min-width: ${breakpoints.xlarge}) {
+    margin-bottom: -400px;
+  }
+  @media screen and (min-width: ${breakpoints.xxlarge}) {
+    margin-bottom: -600px;
+  }
+`
+
+export const MediaWrapper = styled.div`
+  margin: 0 calc(-1 * var(--global-horizontal-padding));
+  width: calc(100% + calc(2 * var(--global-horizontal-padding)));
+`
+
+export const BackgroundImage = styled.div<{ src: string }>`
+  width: 100%;
+  height: 0;
+  padding-top: 56.25%;
+  background-repeat: no-repeat;
+  background-position: center;
+  background-attachment: local;
+  background-size: cover;
+
+  // as the content overlaps the media more and more as the viewport width grows, we need to hide some part of the media with a gradient
+  // this helps with keeping a consistent background behind a page content - we don't want the media to peek out in the content spacing
+  background-image: linear-gradient(0deg, black 0%, rgba(0, 0, 0, 0) 20%), url(${({ src }) => src});
+  @media screen and (min-width: ${breakpoints.small}) {
+    background-image: linear-gradient(0deg, black 0%, rgba(0, 0, 0, 0) 50%), url(${({ src }) => src});
+  }
+  @media screen and (min-width: ${breakpoints.medium}) {
+    background-image: linear-gradient(0deg, black 0%, rgba(0, 0, 0, 0) 70%), url(${({ src }) => src});
+  }
+  @media screen and (min-width: ${breakpoints.large}) {
+    background-image: linear-gradient(0deg, black 0%, black 20%, rgba(0, 0, 0, 0) 90%), url(${({ src }) => src});
+  }
+  @media screen and (min-width: ${breakpoints.xlarge}) {
+    background-image: linear-gradient(0deg, black 0%, black 25%, rgba(0, 0, 0, 0) 90%), url(${({ src }) => src});
+  }
+  @media screen and (min-width: ${breakpoints.xxlarge}) {
+    background-image: linear-gradient(0deg, black 0%, black 30%, rgba(0, 0, 0, 0) 90%), url(${({ src }) => src});
+  }
+`
+
+export const InfoContainer = styled.div`
+  position: relative;
+  margin-top: -${spacing.xxl};
+  padding-bottom: ${spacing.xs};
+
+  @media screen and (min-width: ${breakpoints.small}) {
+    position: absolute;
+    margin: 0;
+    padding: 0;
+    bottom: 15%;
+    max-width: 80%;
+  }
+
+  @media screen and (min-width: ${breakpoints.medium}) {
+    bottom: 30%;
+    max-width: 60%;
+  }
+
+  @media screen and (min-width: ${breakpoints.large}) {
+    bottom: 35%;
+    max-width: 40%;
+  }
+
+  @media screen and (min-width: ${breakpoints.xlarge}) {
+    bottom: 45%;
+  }
+
+  @media screen and (min-width: ${breakpoints.xxlarge}) {
+    bottom: 60%;
+  }
+`
+
+export const ChannelLink = styled(Link)`
+  margin-bottom: ${spacing.m};
+  display: inline-block;
+`
+
+export const StyledAvatar = styled(Avatar)`
+  width: 64px;
+  height: 64px;
+  @media screen and (min-width: ${breakpoints.medium}) {
+    width: 88px;
+    height: 88px;
+  }
+`
+
+export const TitleContainer = styled.div`
+  margin-bottom: ${spacing.xxl};
+  @media screen and (min-width: ${breakpoints.medium}) {
+    margin-bottom: ${spacing.xxxl};
+  }
+
+  h2 {
+    ${fluidRange({ prop: 'fontSize', fromSize: '40px', toSize: '60px' })};
+    ${fluidRange({ prop: 'lineHeight', fromSize: '48px', toSize: '68px' })};
+    font-family: ${typography.fonts.headers};
+    font-weight: 700;
+    margin: 0 0 ${spacing.s} 0;
+    @media screen and (min-width: ${breakpoints.medium}) {
+      margin-bottom: ${spacing.m};
+    }
+  }
+
+  span {
+    ${fluidRange({ prop: 'fontSize', fromSize: '14px', toSize: '28px' })};
+    ${fluidRange({ prop: 'lineHeight', fromSize: '20px', toSize: '30px' })};
+    color: ${colors.white};
+  }
+`
+
+export const PlayButton = styled(Button)`
+  width: 116px;
+`

+ 36 - 0
src/components/FeaturedVideoHeader/FeaturedVideoHeader.tsx

@@ -0,0 +1,36 @@
+import React from 'react'
+import {
+  BackgroundImage,
+  ChannelLink,
+  Container,
+  InfoContainer,
+  MediaWrapper,
+  PlayButton,
+  StyledAvatar,
+  TitleContainer,
+} from './FeaturedVideoHeader.style'
+import { mockCoverVideo, mockCoverVideoChannel } from '@/mocking/data/mockCoverVideo'
+import { navigate } from '@reach/router'
+import routes from '@/config/routes'
+
+const FeaturedVideoHeader: React.FC = () => {
+  return (
+    <Container>
+      <MediaWrapper>
+        <BackgroundImage src={mockCoverVideo.thumbnailURL} />
+      </MediaWrapper>
+      <InfoContainer>
+        <ChannelLink to={routes.channel(mockCoverVideoChannel.id)}>
+          <StyledAvatar img={mockCoverVideoChannel.avatarPhotoURL} name={mockCoverVideoChannel.handle} />
+        </ChannelLink>
+        <TitleContainer>
+          <h2>{mockCoverVideo.title}</h2>
+          <span>{mockCoverVideo.description}</span>
+        </TitleContainer>
+        <PlayButton onClick={() => navigate(routes.video(mockCoverVideo.id))}>Play</PlayButton>
+      </InfoContainer>
+    </Container>
+  )
+}
+
+export default FeaturedVideoHeader

+ 3 - 0
src/components/FeaturedVideoHeader/index.ts

@@ -0,0 +1,3 @@
+import FeaturedVideoHeader from './FeaturedVideoHeader'
+
+export default FeaturedVideoHeader

+ 0 - 46
src/components/Hero.tsx

@@ -1,46 +0,0 @@
-import React from 'react'
-import { fluidRange } from 'polished'
-import { css } from '@emotion/core'
-import { Button, Header } from '@/shared/components'
-
-type HeroProps = {
-  backgroundImg: string
-}
-
-const Hero: React.FC<Partial<HeroProps>> = ({ backgroundImg }) => {
-  return (
-    <Header
-      title="A user governed video platform"
-      subtitle="Lorem ipsum sit amet, consectetur adipiscing elit. Proin non nisl sollicitudin, tempor diam."
-      backgroundImg={backgroundImg}
-      containerCss={css`
-        font-size: 18px;
-        line-height: 1.33;
-        & h1 {
-          ${fluidRange({ prop: 'fontSize', fromSize: '40px', toSize: '72px' })};
-          line-height: 0.94;
-        }
-        margin: 0 calc(-1 * var(--global-horizontal-padding));
-      `}
-    >
-      <div
-        css={css`
-          display: flex;
-          margin-top: 40px;
-          & > * {
-            margin-right: 1rem;
-          }
-        `}
-      >
-        <Button
-          containerCss={css`
-            width: 116px;
-          `}
-        >
-          Play
-        </Button>
-      </div>
-    </Header>
-  )
-}
-export default Hero

+ 1 - 1
src/components/index.ts

@@ -1,6 +1,6 @@
 export { default as LayoutWithRouting } from './LayoutWithRouting'
 export { default as VideoGallery } from './VideoGallery'
-export { default as Hero } from './Hero'
+export { default as FeaturedVideoHeader } from './FeaturedVideoHeader'
 export { default as ChannelGallery } from './ChannelGallery'
 export { default as Navbar } from './Navbar'
 export { default as VideoGrid } from './VideoGrid'

+ 1 - 1
src/mocking/data/mockChannels.ts

@@ -2,7 +2,7 @@ import { channelAvatarSources, channelPosterSources } from './mockImages'
 import { ChannelFields } from '@/api/queries/__generated__/ChannelFields'
 import rawChannels from './raw/channels.json'
 
-type MockChannel = ChannelFields
+export type MockChannel = ChannelFields
 
 const mockChannels: MockChannel[] = rawChannels.map((rawChannel, idx) => ({
   ...rawChannel,

+ 23 - 0
src/mocking/data/mockCoverVideo.ts

@@ -0,0 +1,23 @@
+import rawCoverVideo from './raw/coverVideo.json'
+import { MockVideo } from '@/mocking/data/mockVideos'
+import { MockVideoMedia } from '@/mocking/data/mockVideosMedia'
+import { MockChannel } from '@/mocking/data/mockChannels'
+
+export const mockCoverVideoChannel: MockChannel = {
+  ...rawCoverVideo.channel,
+  __typename: 'Channel',
+}
+
+export const mockCoverVideo: MockVideo = {
+  ...rawCoverVideo.video,
+  __typename: 'Video',
+}
+
+export const mockCoverVideoMedia: MockVideoMedia = {
+  ...rawCoverVideo.videoMedia,
+  __typename: 'VideoMedia',
+  location: {
+    __typename: 'HTTPVideoMediaLocation',
+    ...rawCoverVideo.videoMedia.location,
+  },
+}

+ 1 - 1
src/mocking/data/mockVideos.ts

@@ -2,7 +2,7 @@ import { thumbnailSources } from './mockImages'
 import { VideoFields } from '@/api/queries/__generated__/VideoFields'
 import rawVideos from './raw/videos.json'
 
-type MockVideo = Omit<VideoFields, 'media' | 'category' | 'channel' | 'publishedOnJoystreamAt' | 'duration'> & {
+export type MockVideo = Omit<VideoFields, 'media' | 'category' | 'channel' | 'publishedOnJoystreamAt' | 'duration'> & {
   publishedOnJoystreamAt: unknown
 }
 

+ 1 - 1
src/mocking/data/mockVideosMedia.ts

@@ -1,7 +1,7 @@
 import { VideoMediaFields } from '@/api/queries/__generated__/VideoMediaFields'
 import rawVideosMedia from './raw/videosMedia.json'
 
-type MockVideoMedia = VideoMediaFields & { duration: number }
+export type MockVideoMedia = VideoMediaFields & { duration: number }
 
 const mockVideosMedia: MockVideoMedia[] = rawVideosMedia.map((rawVideoMedia) => {
   return {

+ 25 - 0
src/mocking/data/raw/coverVideo.json

@@ -0,0 +1,25 @@
+{
+  "video": {
+    "id": "2edfa557-9298-43c7-a6c5-95e580ace40e",
+    "title": "Ghost Signals",
+    "description": "How We Lost Trust In Authority, And Authority Taught Us To Distrust Ourselves",
+    "views": 132,
+    "publishedOnJoystreamAt": "2020-11-04T15:59:55.820Z",
+    "thumbnailURL": "https://eu-central-1.linodeobjects.com/atlas-assets/featured-video-thumbnail.jpg"
+  },
+  "videoMedia": {
+    "id": "4551c421-8edd-4d3b-8357-9fe825742775",
+    "codec": "H264_mpeg4",
+    "pixelWidth": 2560,
+    "pixelHeight": 1440,
+    "duration": 29,
+    "size": 44851016,
+    "location": { "URL": "https://eu-central-1.linodeobjects.com/atlas-assets/videos/1.mp4" }
+  },
+  "channel": {
+    "id": "d458e199-e1ae-4ccc-9f42-0ff8fbf9ab81",
+    "handle": "SCHISM",
+    "avatarPhotoURL": "https://eu-central-1.linodeobjects.com/atlas-assets/feautured-video-channel-avatar.png",
+    "coverPhotoURL": null
+  }
+}

+ 35 - 1
src/mocking/server/data.ts

@@ -4,8 +4,11 @@ import faker from 'faker'
 import { mockCategories, mockChannels, mockVideos, mockVideosMedia } from '@/mocking/data'
 import { ChannelFields } from '@/api/queries/__generated__/ChannelFields'
 import { CategoryFields } from '@/api/queries/__generated__/CategoryFields'
+import { mockCoverVideo, mockCoverVideoChannel, mockCoverVideoMedia } from '@/mocking/data/mockCoverVideo'
 
-export const createMockData = (server: any) => {
+type MirageJSServer = any
+
+export const createMockData = (server: MirageJSServer) => {
   const channels = mockChannels.map((channel) => {
     return server.schema.create('Channel', {
       ...channel,
@@ -49,4 +52,35 @@ export const createMockData = (server: any) => {
       views: video.views,
     })
   })
+
+  createCoverVideoData(server, categories)
+}
+
+const createCoverVideoData = (server: MirageJSServer, categories: unknown[]) => {
+  const channel = server.schema.create('Channel', {
+    ...mockCoverVideoChannel,
+  })
+
+  const location = server.schema.create('HTTPVideoMediaLocation', {
+    id: faker.random.uuid(),
+    ...mockCoverVideoMedia.location,
+  })
+
+  const media = server.schema.create('VideoMedia', {
+    ...mockCoverVideoMedia,
+    location,
+  })
+
+  const video = server.schema.create('Video', {
+    ...mockCoverVideo,
+    duration: media.duration,
+    category: categories[0],
+    channel,
+    media,
+  })
+
+  server.create('VideoViewsInfo', {
+    id: video.id,
+    views: video.views,
+  })
 }

+ 1 - 1
src/shared/components/Avatar/Avatar.style.tsx

@@ -12,7 +12,7 @@ const container: StyleFn = (_, { size = 'default' }) => {
     minWidth: width,
     backgroundColor: colors.gray[400],
     color: colors.white,
-    display: 'flex',
+    display: 'inline-flex',
     justifyContent: 'center',
     alignItems: 'center',
 

+ 0 - 44
src/shared/components/Header/Header.style.ts

@@ -1,44 +0,0 @@
-import { makeStyles, StyleFn } from '../../utils'
-import { colors, spacing } from '../../theme'
-
-export type HeaderStyleProps = {
-  backgroundImg?: string
-}
-
-const container: StyleFn = (_, { backgroundImg }) => ({
-  textAlign: 'left',
-  color: colors.white,
-  lineHeight: 1.33,
-  height: 584,
-  backgroundImage: `linear-gradient(0deg, black 0%, rgba(0,0,0,0) 100%), url(${backgroundImg})`,
-  backgroundSize: 'cover',
-  backgroundPosition: 'center',
-  display: 'flex',
-  flexDirection: 'column',
-  justifyContent: 'flex-end',
-})
-
-const content: StyleFn = () => ({
-  marginLeft: spacing.xxl,
-  marginBottom: 85,
-  maxWidth: '39.25rem',
-})
-
-const title: StyleFn = () => ({
-  lineHeight: 1.05,
-  letterSpacing: '-0.01em',
-  fontWeight: 'bold',
-  margin: 0,
-})
-
-const subtitle: StyleFn = () => ({
-  maxWidth: '34rem',
-  marginTop: spacing.m,
-})
-
-export const useCSS = (props: HeaderStyleProps) => ({
-  container: makeStyles([container])(props),
-  content: makeStyles([content])(props),
-  title: makeStyles([title])(props),
-  subtitle: makeStyles([subtitle])(props),
-})

+ 0 - 24
src/shared/components/Header/Header.tsx

@@ -1,24 +0,0 @@
-import React from 'react'
-import { SerializedStyles } from '@emotion/core'
-import { useCSS, HeaderStyleProps } from './Header.style'
-
-type HeaderProps = {
-  title: string
-  subtitle: string
-  backgroundImg: string
-  containerCss: SerializedStyles
-  children: React.ReactNode
-} & HeaderStyleProps
-
-export default function Header({ title, subtitle, children, backgroundImg, containerCss }: Partial<HeaderProps>) {
-  const styles = useCSS({ backgroundImg })
-  return (
-    <section css={[styles.container, containerCss]}>
-      <div css={styles.content}>
-        <h1 css={styles.title}>{title}</h1>
-        {subtitle && <p css={styles.subtitle}>{subtitle}</p>}
-        {children}
-      </div>
-    </section>
-  )
-}

+ 0 - 2
src/shared/components/Header/index.ts

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

+ 0 - 1
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 Header } from './Header'
 export { default as Link } from './Link'
 export { default as NavButton } from './NavButton'
 export { default as RadioButton } from './RadioButton'

+ 1 - 0
src/shared/theme/breakpoints.ts

@@ -3,4 +3,5 @@ export default {
   medium: '1024px',
   large: '1440px',
   xlarge: '1920px',
+  xxlarge: '2560px',
 }

+ 5 - 5
src/views/HomeView.tsx

@@ -1,6 +1,6 @@
 import React from 'react'
 import styled from '@emotion/styled'
-import { ChannelGallery, Hero, VideoGallery } from '@/components'
+import { ChannelGallery, FeaturedVideoHeader, VideoGallery } from '@/components'
 import { RouteComponentProps } from '@reach/router'
 import { useQuery } from '@apollo/client'
 import { InfiniteVideoGrid } from '@/shared/components'
@@ -8,8 +8,7 @@ import { GET_FEATURED_VIDEOS, GET_NEWEST_CHANNELS, GET_NEWEST_VIDEOS } from '@/a
 import { GetFeaturedVideos } from '@/api/queries/__generated__/GetFeaturedVideos'
 import { GetNewestVideos, GetNewestVideosVariables } from '@/api/queries/__generated__/GetNewestVideos'
 import { GetNewestChannels, GetNewestChannelsVariables } from '@/api/queries/__generated__/GetNewestChannels'
-
-const backgroundImg = 'https://eu-central-1.linodeobjects.com/atlas-assets/hero.jpeg'
+import { spacing } from '@/shared/theme'
 
 const NEWEST_VIDEOS_COUNT = 8
 const NEWEST_CHANNELS_COUNT = 8
@@ -32,7 +31,7 @@ const HomeView: React.FC<RouteComponentProps> = () => {
 
   return (
     <>
-      <Hero backgroundImg={backgroundImg} />
+      <FeaturedVideoHeader />
       <Container>
         <VideoGallery title="Newest videos" loading={newestVideosLoading} videos={newestVideos} />
         <VideoGallery
@@ -48,7 +47,8 @@ const HomeView: React.FC<RouteComponentProps> = () => {
 }
 
 const Container = styled.div`
-  margin: 1rem 0;
+  position: relative;
+  margin: ${spacing.xxxxl} 0;
   & > * {
     margin-bottom: 3rem;
   }