Преглед на файлове

Adjust Video Preview component to new designs (#987)

* Adjust Video Preview component to new designs (#982)

* Change margin between avatar and title, adjust play button animation, change size of context menu button
Rafał Pawłow преди 3 години
родител
ревизия
b322aac0eb

+ 1 - 0
src/components/VideoTile.tsx

@@ -41,6 +41,7 @@ export const VideoTile: React.FC<VideoTileProps> = ({ id, onNotFound, ...metaPro
       views={video?.views}
       videoHref={videoHref}
       channelHref={id ? absoluteRoutes.viewer.channel(video?.channel.id) : undefined}
+      onCopyVideoURLClick={() => copyToClipboard(videoHref ? location.origin + videoHref : '')}
       thumbnailUrl={thumbnailPhotoUrl}
       isLoading={loading}
       contentKey={id}

+ 1 - 2
src/shared/components/ContextMenu/ContextMenu.style.ts

@@ -16,7 +16,6 @@ export const StyledContainer = styled.div<ContainerProps>`
   background-color: ${colors.gray[800]};
   width: 200px;
   color: ${colors.white};
-  padding: ${sizes(2)};
   word-break: break-all;
 
   &.menu-enter {
@@ -45,7 +44,7 @@ export const StyledContainer = styled.div<ContainerProps>`
 export const StyledMenuItem = styled.div`
   display: flex;
   align-items: center;
-  padding: ${sizes(3)};
+  padding: ${sizes(4)};
   transition: background-color 200ms ${transitions.easing};
 
   &:hover {

+ 78 - 63
src/shared/components/VideoTileBase/VideoTileBase.styles.tsx

@@ -1,9 +1,9 @@
 import { css } from '@emotion/react'
 import styled from '@emotion/styled'
-import { fluidRange, transparentize } from 'polished'
+import { fluidRange } from 'polished'
 import { Link } from 'react-router-dom'
 
-import { colors, media, sizes, transitions, typography } from '@/shared/theme'
+import { colors, media, sizes, square, transitions, typography, zIndex } from '@/shared/theme'
 
 import { Avatar } from '../Avatar'
 import { IconButton } from '../IconButton'
@@ -32,6 +32,7 @@ export const CoverWrapper = styled.div<MainProps>`
   width: 100%;
   max-width: ${({ main }) => (main ? '650px' : '')};
 `
+
 const clickableAnimation = (clickable: boolean) =>
   clickable
     ? css`
@@ -42,13 +43,19 @@ const clickableAnimation = (clickable: boolean) =>
           opacity: 1;
         }
         ${CoverIconWrapper} {
-          transform: translateY(0);
+          opacity: 1;
+        }
+      `
+    : css`
+        ${CoverHoverOverlay} {
+          opacity: 1;
+          border-color: ${colors.white};
         }
-        ${ProgressOverlay} {
-          bottom: ${HOVER_BORDER_SIZE};
+
+        ${CoverIconWrapper} {
+          opacity: 1;
         }
       `
-    : null
 export const CoverContainer = styled.div<ClickableProps>`
   position: relative;
   width: 100%;
@@ -57,7 +64,11 @@ export const CoverContainer = styled.div<ClickableProps>`
   transition: all ${transitions.timings.regular} ${transitions.easing};
   cursor: ${(props) => (props.clickable ? 'pointer' : 'auto')};
 
-  :hover {
+  :active {
+    ${() => clickableAnimation(false)}
+  }
+
+  :hover:not(:active) {
     ${(props) => clickableAnimation(props.clickable)}
   }
 `
@@ -85,6 +96,14 @@ export const Container = styled.article<MainProps>`
   display: inline-flex;
   flex-direction: column;
   ${({ main }) => main && mainContainerCss}
+
+  :hover {
+    ${() => css`
+      ${KebabMenuIconContainer} {
+        display: flex;
+      }
+    `}
+  }
 `
 
 const mainInfoContainerCss = css`
@@ -96,7 +115,6 @@ const mainInfoContainerCss = css`
 export const InfoContainer = styled.div<MainProps>`
   min-height: 86px;
   display: flex;
-  justify-content: space-between;
   margin-top: ${({ main }) => (main ? sizes(4) : sizes(3))};
   ${({ main }) => main && mainInfoContainerCss};
 `
@@ -105,20 +123,15 @@ 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: ${sizes(2)};
+  margin-right: ${sizes(3)};
 `
 
 export const TextContainer = styled.div`
-  width: calc(100% - 40px);
+  width: calc(100% - 87px);
 `
 
 type MetaContainerProps = { noMarginTop: boolean } & MainProps
 export const MetaContainer = styled.div<MetaContainerProps>`
-  margin-top: ${({ noMarginTop, main }) => {
-    if (noMarginTop) return 0
-    if (main) return sizes(3)
-    return sizes(2)
-  }};
   width: 100%;
 `
 
@@ -134,21 +147,21 @@ type CoverImageProps = {
   darkenImg: boolean
 }
 export const CoverImage = styled.img<CoverImageProps>`
+  ${square('100%')}
+
   display: block;
-  width: 100%;
-  height: 100%;
   ${({ darkenImg }) => darkenImg && `filter: brightness(45%);`}
 `
 
 export const CoverNoImage = styled.div`
-  width: 100%;
-  height: 100%;
+  ${square('100%')}
+
   background: linear-gradient(125deg, rgba(16, 18, 20, 1) 30%, rgba(34, 36, 38, 1) 65%, rgba(16, 18, 20, 1) 100%);
 `
 
 export const CoverThumbnailUploadFailed = styled.div`
-  width: 100%;
-  height: 100%;
+  ${square('100%')}
+
   background: linear-gradient(125deg, rgba(16, 18, 20, 1) 30%, rgba(34, 36, 38, 1) 65%, rgba(16, 18, 20, 1) 100%);
   display: flex;
   flex-direction: column;
@@ -163,12 +176,12 @@ export const CoverHoverOverlay = styled.div`
   bottom: 0;
   left: 0;
   opacity: 0;
-  transition: opacity ${transitions.timings.regular} ${transitions.easing};
-  border: ${HOVER_BORDER_SIZE} solid ${colors.white};
-  background: linear-gradient(180deg, #000 0%, rgba(0, 0, 0, 0) 100%);
+  transition: opacity ${transitions.timings.regular} ${transitions.easing}, border ${transitions.timings.routing} linear;
+  background-color: ${colors.transparentGray[54]};
   display: flex;
   justify-content: center;
   align-items: center;
+  border: ${HOVER_BORDER_SIZE} solid transparent;
 `
 
 export const RemoveButton = styled(IconButton)`
@@ -178,17 +191,15 @@ export const RemoveButton = styled(IconButton)`
 `
 
 export const CoverIconWrapper = styled.div`
-  transform: translateY(40px);
-  transition: all ${transitions.timings.regular} ${transitions.easing};
+  opacity: 0;
+  transition: all ${transitions.timings.regular} ease-out;
 `
 
 export const ProgressOverlay = styled.div`
-  position: absolute;
-  left: 0;
-  right: 0;
-  bottom: 0;
+  position: relative;
   height: ${sizes(1)};
-  background-color: ${colors.white};
+  margin-top: ${sizes(3)};
+  background-color: ${colors.gray[400]};
 `
 
 export const ProgressBar = styled.div`
@@ -198,7 +209,7 @@ export const ProgressBar = styled.div`
   height: 100%;
   max-width: 100%;
   width: 0;
-  background-color: ${colors.blue['500']};
+  background-color: ${colors.gray[50]};
 `
 
 export const CoverVideoPublishingStateOverlay = styled.div`
@@ -208,32 +219,60 @@ export const CoverVideoPublishingStateOverlay = styled.div`
   padding: ${sizes(1)} ${sizes(2)};
   display: flex;
   align-items: center;
-  background-color: ${colors.gray['900']};
-  opacity: 0.7;
+  background-color: ${colors.black};
+  color: ${colors.white};
+  z-index: ${zIndex.overlay};
 `
 
 export const PublishingStateText = styled(Text)`
   margin-left: ${sizes(1.5)};
 `
 
+export const KebabMenuIconContainer = styled.div<{ isActive?: boolean }>`
+  ${square(sizes(9))};
+
+  display: ${({ isActive }) => (isActive ? 'flex' : 'none')};
+  align-items: center;
+  justify-content: center;
+  cursor: pointer;
+  position: relative;
+  border-radius: 100%;
+  transition: all ${transitions.timings.regular} ${transitions.easing};
+  margin-left: auto;
+
+  path {
+    transition: all ${transitions.timings.regular} ${transitions.easing};
+  }
+
+  &:hover {
+    path:not([fill='none']) {
+      fill: ${colors.white};
+    }
+
+    background-color: ${colors.transparentPrimary[18]};
+  }
+`
+
 export const CoverDurationOverlay = styled.div`
   position: absolute;
   bottom: ${sizes(2)};
   right: ${sizes(2)};
-  padding: ${sizes(1)} ${sizes(2)};
+  padding: ${sizes(1.5)} ${sizes(2)};
   background-color: ${colors.black};
   color: ${colors.white};
   font-size: ${typography.sizes.body2};
+  z-index: ${zIndex.overlay};
 `
 
 export const StyledAvatar = styled(Avatar)<ChannelProps>`
-  width: 100%;
-  height: 100%;
+  ${square('100%')}
+
   cursor: ${({ channelClickable }) => (channelClickable ? 'pointer' : 'auto')};
 `
 
 export const TitleHeader = styled(Text)<MainProps & ScalesWithCoverProps & ClickableProps>`
   margin: 0;
+  margin-bottom: ${sizes(2)};
   font-weight: ${typography.weights.bold};
   font-size: calc(${(props) => props.scalingFactor} * ${typography.sizes.h6});
   ${({ main }) => main && fluidRange({ prop: 'fontSize', fromSize: '24px', toSize: '40px' })};
@@ -262,11 +301,11 @@ export const SpacedSkeletonLoader = styled(SkeletonLoader)`
   margin-top: 6px;
 `
 export const CoverSkeletonLoader = styled(SkeletonLoader)`
+  ${square('100%')}
+
   position: absolute;
   top: 0;
   left: 0;
-  width: 100%;
-  height: 100%;
 `
 
 export const CoverTopLeftContainer = styled.div`
@@ -274,27 +313,3 @@ export const CoverTopLeftContainer = styled.div`
   top: ${sizes(2)};
   left: ${sizes(2)};
 `
-
-export const KebabMenuIconContainer = styled.div`
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  width: 40px;
-  height: 40px;
-  cursor: pointer;
-  position: relative;
-  border-radius: 100%;
-  transition: all ${transitions.timings.regular} ${transitions.easing};
-
-  path {
-    transition: all ${transitions.timings.regular} ${transitions.easing};
-  }
-
-  &:hover {
-    path:not([fill='none']) {
-      fill: ${colors.white};
-    }
-
-    background-color: ${transparentize(1 - 0.06, colors.white)};
-  }
-`

+ 57 - 43
src/shared/components/VideoTileBase/VideoTileBase.tsx

@@ -62,7 +62,7 @@ export type VideoTileBaseMetaProps = {
   showMeta?: boolean
   main?: boolean
   removeButton?: boolean
-  onClick?: (e: React.MouseEvent<HTMLElement>) => void
+  onClick?: (event: React.MouseEvent<HTMLElement>) => void
   onChannelClick?: (e: React.MouseEvent<HTMLElement>) => void
   onCoverResize?: (width: number | undefined, height: number | undefined) => void
   onRemoveButtonClick?: (e: React.MouseEvent<HTMLElement>) => void
@@ -88,7 +88,7 @@ export type VideoTilePublisherProps =
       onPullupClick?: undefined
       onOpenInTabClick?: undefined
       onEditVideoClick?: undefined
-      onCopyVideoURLClick?: undefined
+      onCopyVideoURLClick?: () => void
       onDeleteVideoClick?: undefined
     }
 
@@ -172,25 +172,25 @@ export const VideoTileBase: React.FC<VideoTileBaseProps> = ({
   const clickable = (!!onClick || !!videoHref) && !isLoading
   const channelClickable = (!!onChannelClick || !!channelHref) && !isLoading
 
-  const handleChannelClick = (e: React.MouseEvent<HTMLElement>) => {
+  const handleChannelClick = (event: React.MouseEvent<HTMLElement>) => {
     if (!onChannelClick) {
       return
     }
-    onChannelClick(e)
+    onChannelClick(event)
   }
 
-  const createAnchorClickHandler = (href?: string) => (e: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => {
+  const createAnchorClickHandler = (href?: string) => (event: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => {
     if (!href) {
-      e.preventDefault()
+      event.preventDefault()
     }
   }
-  const handleCoverHoverOverlayClick = (e: React.MouseEvent<HTMLElement>) => {
-    onClick?.(e)
+  const handleCoverHoverOverlayClick = (event: React.MouseEvent<HTMLElement>) => {
+    onClick?.(event)
   }
-  const handleRemoveClick = (e: React.MouseEvent<HTMLElement>) => {
+  const handleRemoveClick = (event: React.MouseEvent<HTMLElement>) => {
     if (onRemoveButtonClick) {
-      e.preventDefault()
-      onRemoveButtonClick(e)
+      event.preventDefault()
+      onRemoveButtonClick(event)
     }
   }
   const handleFailedThumbnailLoad = () => {
@@ -198,6 +198,7 @@ export const VideoTileBase: React.FC<VideoTileBaseProps> = ({
       setFailedLoadImage(true)
     }
   }
+
   return (
     <Container main={main} className={className}>
       <CoverWrapper main={main}>
@@ -243,9 +244,9 @@ export const VideoTileBase: React.FC<VideoTileBaseProps> = ({
                           <PullUp
                             // set to true when video is already on the snackbar
                             disabled={!!isPullupDisabled}
-                            onClick={(e) => {
-                              e.preventDefault()
-                              onPullupClick && onPullupClick(e)
+                            onClick={(event) => {
+                              event.preventDefault()
+                              onPullupClick && onPullupClick(event)
                             }}
                           />
                         </CoverTopLeftContainer>
@@ -254,7 +255,7 @@ export const VideoTileBase: React.FC<VideoTileBaseProps> = ({
                         {publisherMode ? (
                           <SvgLargeEdit />
                         ) : (
-                          <SvgOutlineVideo width={48} height={48} viewBox="0 0 24 24" />
+                          <SvgOutlineVideo width={34} height={34} viewBox="0 0 34 34" />
                         )}
                       </CoverIconWrapper>
                       {removeButton && (
@@ -264,17 +265,17 @@ export const VideoTileBase: React.FC<VideoTileBaseProps> = ({
                       )}
                     </CoverHoverOverlay>
                   </Anchor>
-                  {!!progress && (
-                    <ProgressOverlay>
-                      <ProgressBar style={{ width: `${progress}%` }} />
-                    </ProgressOverlay>
-                  )}
                 </CoverImageContainer>
               )}
             </CSSTransition>
           </SwitchTransition>
         </CoverContainer>
       </CoverWrapper>
+      {!!progress && (
+        <ProgressOverlay>
+          <ProgressBar style={{ width: `${progress}%` }} />
+        </ProgressOverlay>
+      )}
       <SwitchTransition>
         <CSSTransition
           key={isLoading ? 'placeholder' : `content-${contentKey}`}
@@ -343,34 +344,47 @@ export const VideoTileBase: React.FC<VideoTileBaseProps> = ({
                 </MetaContainer>
               )}
             </TextContainer>
-            {publisherMode && !isLoading && (
-              <div>
-                <KebabMenuIconContainer onClick={(e) => openContextMenu(e, 200)}>
+            {!isLoading && (
+              <>
+                <KebabMenuIconContainer
+                  onClick={(event) => openContextMenu(event, 200)}
+                  isActive={contextMenuOpts.isActive}
+                >
                   <SvgGlyphMore />
                 </KebabMenuIconContainer>
                 <ContextMenu contextMenuOpts={contextMenuOpts}>
-                  {onOpenInTabClick && (
-                    <ContextMenuItem icon={<SvgGlyphPlay />} onClick={onOpenInTabClick}>
-                      Play in Joystream
-                    </ContextMenuItem>
-                  )}
-                  {onCopyVideoURLClick && (
-                    <ContextMenuItem icon={<SvgGlyphCopy />} onClick={onCopyVideoURLClick}>
-                      Copy video URL
-                    </ContextMenuItem>
-                  )}
-                  {onEditVideoClick && (
-                    <ContextMenuItem icon={<SvgGlyphEdit />} onClick={onEditVideoClick}>
-                      {isDraft ? 'Edit draft' : 'Edit video'}
-                    </ContextMenuItem>
-                  )}
-                  {onDeleteVideoClick && (
-                    <ContextMenuItem icon={<SvgGlyphTrash />} onClick={onDeleteVideoClick}>
-                      {isDraft ? 'Delete draft' : 'Delete video'}
-                    </ContextMenuItem>
+                  {publisherMode ? (
+                    <>
+                      {onOpenInTabClick && (
+                        <ContextMenuItem icon={<SvgGlyphPlay />} onClick={onOpenInTabClick}>
+                          Play in Joystream
+                        </ContextMenuItem>
+                      )}
+                      {onCopyVideoURLClick && (
+                        <ContextMenuItem icon={<SvgGlyphCopy />} onClick={onCopyVideoURLClick}>
+                          Copy video URL
+                        </ContextMenuItem>
+                      )}
+                      {onEditVideoClick && (
+                        <ContextMenuItem icon={<SvgGlyphEdit />} onClick={onEditVideoClick}>
+                          {isDraft ? 'Edit draft' : 'Edit video'}
+                        </ContextMenuItem>
+                      )}
+                      {onDeleteVideoClick && (
+                        <ContextMenuItem icon={<SvgGlyphTrash />} onClick={onDeleteVideoClick}>
+                          {isDraft ? 'Delete draft' : 'Delete video'}
+                        </ContextMenuItem>
+                      )}
+                    </>
+                  ) : (
+                    onCopyVideoURLClick && (
+                      <ContextMenuItem onClick={onCopyVideoURLClick} icon={<SvgGlyphCopy />}>
+                        Copy video URL
+                      </ContextMenuItem>
+                    )
                   )}
                 </ContextMenu>
-              </div>
+              </>
             )}
           </InfoContainer>
         </CSSTransition>

+ 4 - 0
src/shared/components/VideoTileBase/constants.ts

@@ -0,0 +1,4 @@
+export const MIN_VIDEO_PREVIEW_WIDTH = 300
+export const MAX_VIDEO_PREVIEW_WIDTH = 600
+export const MIN_SCALING_FACTOR = 1
+export const MAX_SCALING_FACTOR = 1.375

+ 5 - 12
src/shared/icons/OutlineVideo.tsx

@@ -2,18 +2,11 @@
 import * as React from 'react'
 
 export const SvgOutlineVideo = (props: React.SVGProps<SVGSVGElement>) => (
-  <svg width={24} height={24} viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
-    <rect
-      x={3}
-      y={3}
-      width={18}
-      height={18}
-      rx={9}
-      stroke="#F4F6F8"
-      strokeWidth={2}
-      strokeMiterlimit={10}
-      strokeLinecap="square"
+  <svg viewBox="0 0 34 34" {...props}>
+    <path
+      fill="#F4F6F8"
+      d="M17 34C7.6 34 0 26.4 0 17S7.6 0 17 0s17 7.6 17 17-7.6 17-17 17zm0-32C8.7 2 2 8.7 2 17s6.7 15 15 15 15-6.7 15-15S25.3 2 17 2z"
     />
-    <path d="M10 9l5 3-5 3V9z" fill="#F4F6F8" />
+    <path fill="#F4F6F8" d="M14 11l9 6-9 6V11z" />
   </svg>
 )

+ 3 - 3
src/shared/icons/svgs/outline-video.svg

@@ -1,4 +1,4 @@
-<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
-<rect x="3" y="3" width="18" height="18" rx="9" stroke="#F4F6F8" stroke-width="2" stroke-miterlimit="10" stroke-linecap="square"/>
-<path d="M10 9L15 12L10 15V9Z" fill="#F4F6F8"/>
+<svg version="1.1" x="0px" y="0px" viewBox="0 0 34 34" style="enable-background:new 0 0 34 34;" xml:space="preserve">
+    <path fill="#F4F6F8" d="M17,34C7.6,34,0,26.4,0,17C0,7.6,7.6,0,17,0s17,7.6,17,17C34,26.4,26.4,34,17,34z M17,2C8.7,2,2,8.7,2,17s6.7,15,15,15c8.3,0,15-6.7,15-15S25.3,2,17,2z"/>
+    <path fill="#F4F6F8" d="M14,11l9,6l-9,6V11z"/>
 </svg>

+ 3 - 0
src/shared/theme/colors.ts

@@ -66,6 +66,9 @@ export default {
     66: 'rgba(0,0,0, 0.66)',
     86: 'rgba(0,0,0, 0.86)',
   },
+  transparentGray: {
+    54: 'rgba(24, 28, 32, 0.54)',
+  },
   transparentPrimary: {
     6: 'rgba(180, 187, 255, 0.06)',
     10: 'rgba(180, 187, 255, 0.10)',

+ 3 - 3
src/shared/theme/index.ts

@@ -1,9 +1,9 @@
 import breakpoints from './breakpoints'
 import colors from './colors'
 import media from './media'
-import { sizes, zIndex } from './sizes'
+import { sizes, square, zIndex } from './sizes'
 import transitions from './transitions'
 import typography from './typography'
 
-export { typography, breakpoints, sizes, zIndex, colors, transitions, media }
-export default { typography, breakpoints, sizes, colors, transitions, media }
+export { typography, breakpoints, sizes, zIndex, colors, transitions, media, square }
+export default { typography, breakpoints, sizes, colors, transitions, media, square }

+ 11 - 2
src/shared/theme/sizes.ts

@@ -1,3 +1,5 @@
+import { css } from '@emotion/react'
+
 const base = 4
 
 export function sizes<B extends boolean>(n: number, raw?: B): B extends false ? string : number
@@ -8,11 +10,18 @@ export function sizes(n: number, raw?: boolean) {
 export const zIndex = {
   background: -10,
   farBackground: -20,
+  overlay: 10,
+  nearOverlay: 20,
   header: 100,
   sheetOverlay: 150,
   nearSheetOverlay: 160,
   sideNav: 200,
-  overlay: 10,
-  nearOverlay: 20,
   globalOverlay: 999,
 }
+
+export function square(size: string | number) {
+  return css`
+    width: ${size};
+    height: ${size};
+  `
+}