Browse Source

Merge pull request #1665 from Gamaranto/make-preview-fluid

Make Preview Elements Resize Accordingly To Preview Width
Bedeho Mender 4 years ago
parent
commit
2e7e5ebc97

+ 3 - 11
src/components/ChannelGallery.tsx

@@ -20,13 +20,13 @@ const ChannelGallery: React.FC<ChannelGalleryProps> = ({ title, channels, loadin
   const displayPlaceholders = loading || !channels
 
   return (
-    <Gallery title={title} trackPadding={trackPadding}>
+    <Gallery title={title} trackPadding={trackPadding} itemWidth={210}>
       {displayPlaceholders
         ? Array.from({ length: PLACEHOLDERS_COUNT }).map((_, idx) => (
-            <StyledChannelPreviewBase key={`channel-placeholder-${idx}`} />
+            <ChannelPreviewBase key={`channel-placeholder-${idx}`} />
           ))
         : channels!.map((channel) => (
-            <StyledChannelPreview
+            <ChannelPreview
               id={channel.id}
               name={channel.handle}
               avatarURL={channel.avatarPhotoURL}
@@ -38,12 +38,4 @@ const ChannelGallery: React.FC<ChannelGalleryProps> = ({ title, channels, loadin
   )
 }
 
-const StyledChannelPreviewBase = styled(ChannelPreviewBase)`
-  margin-right: 1.5rem;
-`
-
-const StyledChannelPreview = styled(ChannelPreview)`
-  margin-right: 1.5rem;
-`
-
 export default ChannelGallery

+ 3 - 3
src/components/VideoGallery.tsx

@@ -3,7 +3,7 @@ import { BreakPoint } from 'react-glider'
 
 import styled from '@emotion/styled'
 
-import { breakpointsOfGrid, Gallery, MAX_VIDEO_PREVIEW_WIDTH, VideoPreviewBase } from '@/shared/components'
+import { breakpointsOfGrid, Gallery, MIN_VIDEO_PREVIEW_WIDTH, VideoPreviewBase } from '@/shared/components'
 import VideoPreview from './VideoPreviewWithNavigation'
 import { VideoFields } from '@/api/queries/__generated__/VideoFields'
 
@@ -37,7 +37,7 @@ const VideoGallery: React.FC<VideoGalleryProps> = ({ title, videos, loading }) =
   const displayPlaceholders = loading || !videos
 
   return (
-    <Gallery title={title} trackPadding={trackPadding} responsive={breakpoints}>
+    <Gallery title={title} trackPadding={trackPadding} responsive={breakpoints} itemWidth={MIN_VIDEO_PREVIEW_WIDTH}>
       {displayPlaceholders
         ? Array.from({ length: PLACEHOLDERS_COUNT }).map((_, idx) => (
             <StyledVideoPreviewBase key={`video-placeholder-${idx}`} />
@@ -65,7 +65,7 @@ const videoPreviewCss = css`
     margin-left: ${sizes.b6}px;
   }
 
-  min-width: ${MAX_VIDEO_PREVIEW_WIDTH};
+  min-width: ${MIN_VIDEO_PREVIEW_WIDTH};
 `
 
 const StyledVideoPreviewBase = styled(VideoPreviewBase)`

+ 5 - 0
src/shared/__tests__/VideoPreview.test.tsx

@@ -1,7 +1,12 @@
 import React from 'react'
 import { mount } from 'enzyme'
 import { VideoPreview } from '@/shared/components/VideoPreview'
+
 describe('VideoPreview component', () => {
+  ;(global as any).ResizeObserver = class {
+    disconnect() {}
+    observe(element: any, initObject: any) {}
+  }
   it('Should render Video Preview correctly', () => {
     expect(
       mount(

+ 0 - 2
src/shared/components/Carousel/Carousel.tsx

@@ -23,7 +23,6 @@ const Carousel: React.FC<CarouselProps> = ({
   children,
   trackPadding = '0',
   className,
-  itemWidth = 300,
   slidesToShow = 'auto',
   ...gliderProps
 }) => {
@@ -63,7 +62,6 @@ const Carousel: React.FC<CarouselProps> = ({
         ref={gliderRef as React.RefObject<GliderMethods>}
         iconLeft={LeftArrow}
         iconRight={RightArrow}
-        itemWidth={itemWidth}
         slidesToShow={slidesToShow}
         // Akward conversion needed until this is resolved: https://github.com/hipstersmoothie/react-glider/issues/36
         arrows={(arrows as unknown) as { prev: string; next: string }}

+ 2 - 1
src/shared/components/Grid/Grid.tsx

@@ -2,6 +2,7 @@ import React from 'react'
 import styled from '@emotion/styled'
 import useResizeObserver from 'use-resize-observer'
 import { spacing, breakpoints } from '../../theme'
+import { MIN_VIDEO_PREVIEW_WIDTH } from '../VideoPreview'
 
 const toPx = (n: number | string) => (typeof n === 'number' ? `${n}px` : n)
 
@@ -34,7 +35,7 @@ const Grid: React.FC<GridProps> = ({
   onResize,
   repeat = 'fit',
   maxColumns = 6,
-  minWidth = 300,
+  minWidth = MIN_VIDEO_PREVIEW_WIDTH,
   ...props
 }) => {
   const { ref: gridRef } = useResizeObserver<HTMLDivElement>({

+ 11 - 8
src/shared/components/VideoPreview/VideoPreview.styles.tsx

@@ -16,6 +16,10 @@ type ChannelProps = {
   channelClickable: boolean
 }
 
+type ScalesWithCoverProps = {
+  scalingFactor: number
+}
+
 export const CoverImage = styled.img<CoverImageProps>`
   display: block;
   position: absolute;
@@ -87,23 +91,22 @@ export const StyledAvatar = styled(Avatar)<ChannelProps>`
   cursor: ${({ channelClickable }) => (channelClickable ? 'pointer' : 'auto')};
 `
 
-export const TitleHeader = styled.h3<MainProps>`
+export const TitleHeader = styled.h3<MainProps & ScalesWithCoverProps>`
   margin: 0;
   font-weight: ${typography.weights.bold};
-  font-size: ${typography.sizes.h6};
+  font-size: calc(${(props) => props.scalingFactor} * ${typography.sizes.h6});
   ${({ main }) => main && fluidRange({ prop: 'fontSize', fromSize: '24px', toSize: '40px' })};
   line-height: ${({ main }) => (main ? 1 : 1.25)};
-  color: ${colors.white};
-  display: inline-block;
 `
 
-export const ChannelName = styled.span<ChannelProps>`
-  font-size: ${typography.sizes.subtitle2};
+export const ChannelName = styled.span<ChannelProps & ScalesWithCoverProps>`
+  font-size: calc(${(props) => props.scalingFactor} * ${typography.sizes.subtitle2});
   line-height: 1.25rem;
   display: inline-block;
   cursor: ${({ channelClickable }) => (channelClickable ? 'pointer' : 'auto')};
 `
 
-export const MetaText = styled.span<MainProps>`
-  font-size: ${({ main }) => (main ? typography.sizes.h6 : typography.sizes.subtitle2)};
+export const MetaText = styled.span<MainProps & ScalesWithCoverProps>`
+  font-size: ${({ main, scalingFactor }) =>
+    main ? typography.sizes.h6 : `calc(${scalingFactor}*${typography.sizes.subtitle2})`};
 `

+ 35 - 5
src/shared/components/VideoPreview/VideoPreview.tsx

@@ -1,4 +1,6 @@
-import React from 'react'
+import React, { useState } from 'react'
+import useResizeObserver from 'use-resize-observer'
+
 import {
   ChannelName,
   CoverDurationOverlay,
@@ -15,6 +17,16 @@ import { formatDurationShort } from '@/utils/time'
 import VideoPreviewBase from './VideoPreviewBase'
 import { formatVideoViewsAndDate } from '@/utils/video'
 
+export const MIN_VIDEO_PREVIEW_WIDTH = 300
+const MAX_VIDEO_PREVIEW_WIDTH = 600
+const MIN_SCALING_FACTOR = 1
+const MAX_SCALING_FACTOR = 1.375
+// Linear Interpolation, see https://en.wikipedia.org/wiki/Linear_interpolation
+const calculateScalingFactor = (videoPreviewWidth: number) =>
+  MIN_SCALING_FACTOR +
+  ((videoPreviewWidth - MIN_VIDEO_PREVIEW_WIDTH) * (MAX_SCALING_FACTOR - MIN_SCALING_FACTOR)) /
+    (MAX_VIDEO_PREVIEW_WIDTH - MIN_VIDEO_PREVIEW_WIDTH)
+
 type VideoPreviewProps = {
   title: string
   channelName: string
@@ -48,11 +60,20 @@ const VideoPreview: React.FC<VideoPreviewProps> = ({
   showChannel = true,
   showMeta = true,
   main = false,
-  imgRef,
   onClick,
   onChannelClick,
   className,
 }) => {
+  const [scalingFactor, setScalingFactor] = useState(MIN_SCALING_FACTOR)
+  const { ref: imgRef } = useResizeObserver<HTMLImageElement>({
+    onResize: (size) => {
+      const { width: videoPreviewWidth } = size
+      if (videoPreviewWidth && !main) {
+        setScalingFactor(calculateScalingFactor(videoPreviewWidth))
+      }
+    },
+  })
+
   const channelClickable = !!onChannelClick
 
   const handleChannelClick = (e: React.MouseEvent<HTMLElement>) => {
@@ -86,7 +107,11 @@ const VideoPreview: React.FC<VideoPreviewProps> = ({
     </>
   )
 
-  const titleNode = <TitleHeader main={main}>{title}</TitleHeader>
+  const titleNode = (
+    <TitleHeader main={main} scalingFactor={scalingFactor}>
+      {title}
+    </TitleHeader>
+  )
 
   const channelAvatarNode = (
     <StyledAvatar
@@ -99,12 +124,16 @@ const VideoPreview: React.FC<VideoPreviewProps> = ({
   )
 
   const channelNameNode = (
-    <ChannelName channelClickable={channelClickable} onClick={handleChannelClick}>
+    <ChannelName channelClickable={channelClickable} onClick={handleChannelClick} scalingFactor={scalingFactor}>
       {channelName}
     </ChannelName>
   )
 
-  const metaNode = <MetaText main={main}>{formatVideoViewsAndDate(views, createdAt, { fullViews: main })}</MetaText>
+  const metaNode = (
+    <MetaText main={main} scalingFactor={scalingFactor}>
+      {formatVideoViewsAndDate(views, createdAt, { fullViews: main })}
+    </MetaText>
+  )
 
   return (
     <VideoPreviewBase
@@ -118,6 +147,7 @@ const VideoPreview: React.FC<VideoPreviewProps> = ({
       metaNode={metaNode}
       onClick={onClick && handleClick}
       className={className}
+      scalingFactor={scalingFactor}
     />
   )
 }

+ 7 - 5
src/shared/components/VideoPreview/VideoPreviewBase.styles.tsx

@@ -13,7 +13,9 @@ type ContainerProps = {
   clickable: boolean
 } & MainProps
 
-export const MAX_VIDEO_PREVIEW_WIDTH = '320px'
+type ScalesWithCoverProps = {
+  scalingFactor: number
+}
 
 const fadeIn = keyframes`
   0% {
@@ -93,10 +95,10 @@ export const InfoContainer = styled.div<MainProps>`
   ${({ main }) => main && mainInfoContainerCss};
 `
 
-export const AvatarContainer = styled.div`
-  width: 40px;
-  min-width: 40px;
-  height: 40px;
+export const AvatarContainer = styled.div<ScalesWithCoverProps>`
+  width: calc(40px * ${(props) => props.scalingFactor});
+  min-width: calc(40px * ${(props) => props.scalingFactor});
+  height: calc(40px * ${(props) => props.scalingFactor});
   margin-right: ${spacing.xs};
 `
 

+ 7 - 1
src/shared/components/VideoPreview/VideoPreviewBase.tsx

@@ -22,6 +22,7 @@ type VideoPreviewBaseProps = {
   metaNode?: React.ReactNode
   onClick?: (e: React.MouseEvent<HTMLElement>) => void
   className?: string
+  scalingFactor?: number
 }
 
 const VideoPreviewBase: React.FC<VideoPreviewBaseProps> = ({
@@ -35,6 +36,7 @@ const VideoPreviewBase: React.FC<VideoPreviewBaseProps> = ({
   metaNode,
   onClick,
   className,
+  scalingFactor = 1,
 }) => {
   const clickable = !!onClick
 
@@ -52,7 +54,11 @@ const VideoPreviewBase: React.FC<VideoPreviewBaseProps> = ({
         <CoverContainer>{coverNode || coverPlaceholder}</CoverContainer>
       </CoverWrapper>
       <InfoContainer main={main}>
-        {displayChannel && <AvatarContainer>{channelAvatarNode || channelAvatarPlaceholder}</AvatarContainer>}
+        {displayChannel && (
+          <AvatarContainer scalingFactor={scalingFactor}>
+            {channelAvatarNode || channelAvatarPlaceholder}
+          </AvatarContainer>
+        )}
         <TextContainer>
           {titleNode || titlePlaceholder}
           {displayChannel && (channelNameNode || channelNamePlaceholder)}

+ 2 - 3
src/shared/components/VideoPreview/index.tsx

@@ -1,5 +1,4 @@
-import VideoPreview from './VideoPreview'
+import VideoPreview, { MIN_VIDEO_PREVIEW_WIDTH } from './VideoPreview'
 import VideoPreviewBase from './VideoPreviewBase'
-import { MAX_VIDEO_PREVIEW_WIDTH } from './VideoPreviewBase.styles'
 
-export { VideoPreview, VideoPreviewBase, MAX_VIDEO_PREVIEW_WIDTH }
+export { VideoPreview, VideoPreviewBase, MIN_VIDEO_PREVIEW_WIDTH }

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

@@ -10,7 +10,7 @@ export { default as Tabs } from './Tabs'
 export { default as Tab } from './Tabs/Tab'
 export { default as Tag } from './Tag'
 export { default as Typography } from './Typography'
-export { VideoPreview, VideoPreviewBase, MAX_VIDEO_PREVIEW_WIDTH } from './VideoPreview'
+export { VideoPreview, VideoPreviewBase, MIN_VIDEO_PREVIEW_WIDTH } from './VideoPreview'
 export { default as VideoPlayer } from './VideoPlayer'
 export { default as SeriesPreview } from './SeriesPreview'
 export { ChannelPreview, ChannelPreviewBase } from './ChannelPreview'