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

Sidenavbar update (#228)

* redesign SideNavbar

* create FollowingChannels component

* add logo to sidenav

* refactor FollowingChannels

* refactor transitions

* change name of FollowingChannels, use personal data hook

* update routes in LayoutWithRouting

* handle channel id change in infinite video grid

* fix issue with non-boolean attribute

* move SideNavbar to components

* fix ChannelLink cache fetch

* update logo posiiton

* remove transition-delay on SidebarNavLink

Co-authored-by: Klaudiusz Dembler <dev@kdembler.com>
Bartosz Dryl 4 éve
szülő
commit
e2cff1f658

+ 9 - 0
src/api/client/cache.ts

@@ -13,6 +13,15 @@ const cache = new InMemoryCache({
           const categoryId = args?.where?.categoryId_eq || ''
           return `${channelId}:${categoryId}`
         }),
+        channel(existing, { toReference, args }) {
+          return (
+            existing ||
+            toReference({
+              __typename: 'Channel',
+              id: args?.where.id,
+            })
+          )
+        },
       },
     },
     Video: {

+ 12 - 14
src/components/ChannelLink/ChannelLink.tsx

@@ -1,7 +1,7 @@
-import React, { useMemo } from 'react'
-import { useApolloClient } from '@apollo/client'
-import { basicChannelFieldsFragment } from '@/api/queries'
-import { GetChannelVariables } from '@/api/queries/__generated__/GetChannel'
+import React from 'react'
+import { useQuery } from '@apollo/client'
+import { GET_CHANNEL } from '@/api/queries'
+import { GetChannel, GetChannelVariables } from '@/api/queries/__generated__/GetChannel'
 import { BasicChannelFields } from '@/api/queries/__generated__/BasicChannelFields'
 import Avatar, { AvatarSize } from '@/shared/components/Avatar'
 import routes from '@/config/routes'
@@ -27,17 +27,15 @@ const ChannelLink: React.FC<ChannelLinkProps> = ({
   avatarSize = 'default',
   className,
 }) => {
-  const client = useApolloClient()
+  const { data } = useQuery<GetChannel, GetChannelVariables>(GET_CHANNEL, {
+    fetchPolicy: 'cache-first',
+    skip: !id,
+    variables: {
+      id: id || '',
+    },
+  })
 
-  const channel = useMemo(() => {
-    if (!id) {
-      return null
-    }
-    return client.readFragment<BasicChannelFields, GetChannelVariables>({
-      fragment: basicChannelFieldsFragment,
-      id: `Channel:${id}`,
-    })
-  }, [id, client])
+  const channel = data?.channel
 
   const displayedChannel = overrideChannel || channel
 

+ 16 - 1
src/components/InfiniteGrids/InfiniteVideoGrid.tsx

@@ -24,7 +24,7 @@ const INITIAL_VIDEOS_PER_ROW = 4
 const InfiniteVideoGrid: React.FC<InfiniteVideoGridProps> = ({
   title,
   categoryId = '',
-  channelId,
+  channelId = null,
   skipCount = 0,
   ready = true,
   showChannel = true,
@@ -39,6 +39,7 @@ const InfiniteVideoGrid: React.FC<InfiniteVideoGridProps> = ({
   const [targetRowsCountByCategory, setTargetRowsCountByCategory] = useState<Record<string, number>>({
     [categoryId]: INITIAL_ROWS,
   })
+  const [cachedChannelId, setCachedChannelId] = useState<string | null>(channelId)
   const [cachedCategoryId, setCachedCategoryId] = useState<string>(categoryId)
 
   const targetRowsCount = targetRowsCountByCategory[cachedCategoryId]
@@ -93,6 +94,20 @@ const InfiniteVideoGrid: React.FC<InfiniteVideoGridProps> = ({
     }
   }, [categoryId, channelId, cachedCategoryId, targetRowsCountByCategory, videosPerRow, skipCount])
 
+  // handle channel change
+  useEffect(() => {
+    if (channelId === cachedChannelId) {
+      return
+    }
+
+    setCachedChannelId(channelId)
+
+    setQueryVariables({
+      ...(channelId ? { channelId } : {}),
+      ...(categoryId ? { categoryId } : {}),
+    })
+  }, [channelId, cachedChannelId, categoryId])
+
   const gridContent = (
     <>
       {displayedItems.map((v) => (

+ 62 - 0
src/components/SideNavbar/FollowedChannels.style.ts

@@ -0,0 +1,62 @@
+import { sizes, colors, typography } from '@/shared/theme'
+import styled from '@emotion/styled'
+import ChannelLink from '../ChannelLink'
+import Text from '../../shared/components/Text'
+import { NAVBAR_LEFT_PADDING, EXPANDED_SIDENAVBAR_WIDTH } from './SideNavbar.style'
+
+export const ChannelsTitle = styled(Text)`
+  margin-top: ${sizes(6)};
+  margin-bottom: ${sizes(4)};
+  padding-left: ${NAVBAR_LEFT_PADDING}px;
+
+  width: ${EXPANDED_SIDENAVBAR_WIDTH - NAVBAR_LEFT_PADDING}px;
+
+  color: ${colors.gray[300]};
+`
+export const ChannelsWrapper = styled.div`
+  padding-left: ${NAVBAR_LEFT_PADDING}px;
+  width: ${EXPANDED_SIDENAVBAR_WIDTH}px;
+  overflow-y: auto;
+  overflow-x: hidden;
+  margin-bottom: 60px;
+`
+
+export const ChannelsList = styled.ul`
+  width: ${EXPANDED_SIDENAVBAR_WIDTH - NAVBAR_LEFT_PADDING}px;
+  overflow-x: hidden;
+  padding: 0;
+  margin: 0;
+`
+export const StyledChannelLink = styled(ChannelLink)`
+  span {
+    margin-left: ${sizes(6)};
+  }
+`
+
+export const ChannelsItem = styled.li`
+  margin-top: ${sizes(5)};
+  list-style: none;
+`
+
+export const ShowMoreButton = styled.button`
+  border: none;
+  background: none;
+
+  font-family: ${typography.fonts.base};
+  font-size: 1rem;
+  font-weight: bold;
+
+  cursor: pointer;
+  padding: ${sizes(5)} 0;
+  display: flex;
+  align-items: center;
+  color: ${colors.white};
+  svg {
+    margin-top: 2px;
+    margin-left: 10px;
+  }
+
+  > span {
+    margin-left: 34px;
+  }
+`

+ 65 - 0
src/components/SideNavbar/FollowedChannels.tsx

@@ -0,0 +1,65 @@
+import React, { useState } from 'react'
+import {
+  ChannelsWrapper,
+  ChannelsTitle,
+  ChannelsList,
+  ChannelsItem,
+  ShowMoreButton,
+  StyledChannelLink,
+} from './FollowedChannels.style'
+import { transitions } from '@/shared/theme'
+import { CSSTransition } from 'react-transition-group'
+import Icon from '../../shared/components/Icon'
+import { FollowedChannel } from '@/hooks/usePersonalData/localStorageClient/types'
+
+const MAX_CHANNELS = 4
+
+type FollowedChannelsProps = {
+  followedChannels: FollowedChannel[]
+  expanded: boolean
+  onClick: () => void
+}
+
+const FollowedChannels: React.FC<FollowedChannelsProps> = ({ followedChannels, expanded, onClick }) => {
+  const [isShowingMore, setIsShowingMore] = useState(false)
+
+  const numberOfChannels = followedChannels.length
+  const channelsToSlice = isShowingMore ? numberOfChannels : MAX_CHANNELS
+  const channels = followedChannels.slice(0, channelsToSlice)
+  return (
+    <>
+      <CSSTransition
+        in={expanded}
+        unmountOnExit
+        timeout={parseInt(transitions.timings.loading)}
+        classNames={transitions.names.fade}
+      >
+        <ChannelsTitle variant="h6">Followed Channels</ChannelsTitle>
+      </CSSTransition>
+      <CSSTransition
+        in={expanded}
+        unmountOnExit
+        timeout={parseInt(transitions.timings.loading)}
+        classNames={transitions.names.fade}
+      >
+        <ChannelsWrapper>
+          <ChannelsList>
+            {channels.map(({ id }) => (
+              <ChannelsItem key={id} onClick={onClick}>
+                <StyledChannelLink id={id} />
+              </ChannelsItem>
+            ))}
+          </ChannelsList>
+          {numberOfChannels > MAX_CHANNELS && (
+            <ShowMoreButton onClick={() => setIsShowingMore(!isShowingMore)}>
+              <Icon name={isShowingMore ? 'chevron-up' : 'chevron-down'} />
+              {isShowingMore ? <span>Show Less</span> : <span>Show {numberOfChannels - MAX_CHANNELS} More</span>}
+            </ShowMoreButton>
+          )}
+        </ChannelsWrapper>
+      </CSSTransition>
+    </>
+  )
+}
+
+export default FollowedChannels

+ 56 - 24
src/shared/components/SideNavbar/SideNavbar.style.ts → src/components/SideNavbar/SideNavbar.style.tsx

@@ -1,10 +1,12 @@
 import styled from '@emotion/styled'
-import Icon from '@/shared/components/Icon'
-import { breakpoints, colors, sizes, transitions, typography, zIndex } from '../../theme'
+import { breakpoints, colors, sizes, transitions, typography, zIndex } from '../../shared/theme'
+import React from 'react'
 import { Link } from '@reach/router'
+import { ReactComponent as UnstyledFullLogo } from '@/assets/full-logo.svg'
 
-export const SIDENAVBAR_WIDTH = 56
+export const SIDENAVBAR_WIDTH = 72
 export const EXPANDED_SIDENAVBAR_WIDTH = 360
+export const NAVBAR_LEFT_PADDING = 24
 
 type ExpandableElementProps = {
   expanded: boolean
@@ -14,6 +16,10 @@ type SubItemProps = {
   subitemsHeight?: number
 } & ExpandableElementProps
 
+type SidebarNavLinkProps = {
+  content: string
+} & ExpandableElementProps
+
 export const SidebarNav = styled.nav<ExpandableElementProps>`
   position: fixed;
   top: 0;
@@ -28,42 +34,59 @@ export const SidebarNav = styled.nav<ExpandableElementProps>`
 
   overflow: hidden;
   color: ${colors.white};
-  background-color: ${colors.blue[700]};
+  background-color: ${colors.gray[700]};
   @media screen and (min-width: ${breakpoints.medium}) {
     left: 0;
     width: ${({ expanded }) => (expanded ? EXPANDED_SIDENAVBAR_WIDTH : SIDENAVBAR_WIDTH)}px;
   }
 `
 
+export const LogoLink = styled(Link)`
+  margin-top: 24px;
+  margin-left: 80px;
+  @media screen and (min-width: ${breakpoints.medium}) {
+    margin-left: 86px;
+  }
+`
+
+export const Logo = styled(UnstyledFullLogo)`
+  height: ${sizes(8)};
+`
+
 export const SidebarNavList = styled.ul`
+  margin-top: 56px;
   list-style: none;
-  margin-top: 90px;
+  width: ${EXPANDED_SIDENAVBAR_WIDTH}px;
   padding: 0;
-  padding: ${sizes(8)} ${sizes(4)};
 `
 
 export const SidebarNavItem = styled.li`
-  &:not(:first-child) {
-    margin-top: ${sizes(10)};
-  }
+  width: 100%;
   display: flex;
   flex-direction: column;
 `
 
-export const ActiveIcon = styled(Icon)`
-  display: none;
-`
-export const InactiveIcon = styled(Icon)`
-  display: block;
-`
-
-export const SidebarNavLink = styled(Link)`
+export const SidebarNavLink = styled(({ expanded, ...props }) => <Link {...props} />)<SidebarNavLinkProps>`
+  padding: ${sizes(5)} ${NAVBAR_LEFT_PADDING}px;
   color: ${colors.white};
   text-decoration: none;
   display: flex;
+  position: relative;
   align-items: center;
   &:hover {
-    opacity: 0.7;
+    background-color: rgba(0, 0, 0, 0.12);
+  }
+  &:focus {
+    background-color: rgba(0, 0, 0, 0.24);
+  }
+  &:active {
+    background-color: rgba(0, 0, 0, 0.4);
+  }
+  > svg {
+    @media screen and (min-width: ${breakpoints.medium}) {
+      transform: translateY(${({ expanded }) => (expanded ? 0 : -8)}px);
+      transition: transform ${transitions.timings.regular} ${transitions.easing};
+    }
   }
   > span {
     margin-left: ${sizes(8)};
@@ -73,16 +96,25 @@ export const SidebarNavLink = styled(Link)`
     line-height: 1;
   }
   &[data-active='true'] {
-    ${ActiveIcon} {
-      display: block;
-    }
-    ${InactiveIcon} {
-      display: none;
+    background-color: rgba(0, 0, 0, 0.24);
+  }
+  :after {
+    @media screen and (min-width: ${breakpoints.medium}) {
+      content: ${({ content }) => `'${content}'`};
+      position: absolute;
+      font-size: 12px;
+      color: white;
+      transition: opacity ${transitions.timings.regular} ${transitions.easing};
+      opacity: ${({ expanded }) => (expanded ? 0 : 1)};
+      left: ${SIDENAVBAR_WIDTH / 2}px;
+      transform: translateX(-50%);
+      bottom: 0;
+      margin-bottom: 10px;
     }
   }
 `
 
-export const DrawerOverlay = styled.div<ExpandableElementProps>`
+export const DrawerOverlay = styled.div`
   position: fixed;
   top: 0;
   right: 0;

+ 24 - 11
src/shared/components/SideNavbar/SideNavbar.tsx → src/components/SideNavbar/SideNavbar.tsx

@@ -1,11 +1,7 @@
 import React, { useState } from 'react'
 import { LinkGetProps } from '@reach/router'
 import useResizeObserver from 'use-resize-observer'
-import HamburgerButton from '../HamburgerButton'
-import { IconType } from '../../icons'
 import {
-  InactiveIcon,
-  ActiveIcon,
   SidebarNav,
   SidebarNavList,
   SidebarNavItem,
@@ -13,9 +9,15 @@ import {
   DrawerOverlay,
   SubItem,
   SubItemsWrapper,
+  Logo,
+  LogoLink,
 } from './SideNavbar.style'
 import { CSSTransition } from 'react-transition-group'
 import { transitions } from '@/shared/theme'
+import Icon, { IconType } from '@/shared/components/Icon'
+import FollowedChannels from './FollowedChannels'
+import { usePersonalData } from '@/hooks'
+import HamburgerButton from '@/shared/components/HamburgerButton'
 
 type NavSubitem = {
   name: string
@@ -23,7 +25,6 @@ type NavSubitem = {
 type NavItemType = {
   subitems?: NavSubitem[]
   icon: IconType
-  iconFilled: IconType
   to: string
 } & NavSubitem
 
@@ -32,8 +33,13 @@ type SidenavProps = {
 }
 
 const SideNavbar: React.FC<SidenavProps> = ({ items }) => {
+  const {
+    state: { followedChannels },
+  } = usePersonalData()
   const [expanded, setExpanded] = useState(false)
 
+  const closeSideNav = () => setExpanded(false)
+
   return (
     <>
       <CSSTransition
@@ -42,10 +48,13 @@ const SideNavbar: React.FC<SidenavProps> = ({ items }) => {
         timeout={parseInt(transitions.timings.loading)}
         classNames={transitions.names.fade}
       >
-        <DrawerOverlay onClick={() => setExpanded(false)} expanded={expanded} />
+        <DrawerOverlay onClick={closeSideNav} />
       </CSSTransition>
       <HamburgerButton active={expanded} onClick={() => setExpanded(!expanded)} />
       <SidebarNav expanded={expanded}>
+        <LogoLink to="/" onClick={closeSideNav}>
+          <Logo />
+        </LogoLink>
         <SidebarNavList>
           {items.map((item) => (
             <NavItem
@@ -53,14 +62,17 @@ const SideNavbar: React.FC<SidenavProps> = ({ items }) => {
               to={item.to}
               expanded={expanded}
               subitems={item.subitems}
-              onClick={() => setExpanded(false)}
+              itemName={item.name}
+              onClick={closeSideNav}
             >
-              <ActiveIcon name={item.iconFilled} />
-              <InactiveIcon name={item.icon} />
+              <Icon name={item.icon} />
               <span>{item.name}</span>
             </NavItem>
           ))}
         </SidebarNavList>
+        {followedChannels.length > 0 && (
+          <FollowedChannels onClick={closeSideNav} followedChannels={followedChannels} expanded={expanded} />
+        )}
       </SidebarNav>
     </>
   )
@@ -70,15 +82,16 @@ type NavItemProps = {
   subitems?: NavSubitem[]
   expanded: boolean
   to: string
+  itemName: string
   onClick: (e: React.MouseEvent<HTMLAnchorElement>) => void
 }
 
-const NavItem: React.FC<NavItemProps> = ({ expanded, subitems, children, to, onClick }) => {
+const NavItem: React.FC<NavItemProps> = ({ expanded = false, subitems, children, to, onClick, itemName }) => {
   const { height: subitemsHeight, ref: subitemsRef } = useResizeObserver<HTMLUListElement>()
 
   return (
     <SidebarNavItem>
-      <SidebarNavLink onClick={onClick} to={to} getProps={isActive}>
+      <SidebarNavLink onClick={onClick} to={to} getProps={isActive} expanded={expanded} content={itemName}>
         {children}
       </SidebarNavLink>
       {subitems && (

+ 0 - 0
src/shared/components/SideNavbar/index.ts → src/components/SideNavbar/index.ts


+ 10 - 5
src/components/TopNavbar/TopNavbar.style.tsx

@@ -5,6 +5,7 @@ import { breakpoints, colors, sizes, transitions, zIndex } from '@/shared/theme'
 import { ReactComponent as UnstyledShortLogo } from '@/assets/logo.svg'
 import { ReactComponent as UnstyledFullLogo } from '@/assets/full-logo.svg'
 import { Link } from '@reach/router'
+import { SIDENAVBAR_WIDTH } from '@/components/SideNavbar'
 
 type TopNavbarStyleProps = {
   hasFocus: boolean
@@ -43,8 +44,8 @@ export const Header = styled.header<TopNavbarStyleProps>`
     column-gap: ${sizes(2)};
   }
   @media screen and (min-width: ${breakpoints.medium}) {
-    margin-left: ${sizes(14)};
-    width: calc(100% - ${sizes(14)});
+    margin-left: ${SIDENAVBAR_WIDTH}px;
+    width: calc(100% - ${SIDENAVBAR_WIDTH}px);
   }
 
   ${StyledSearchbar} {
@@ -59,11 +60,15 @@ export const LogoLink = styled(Link)`
   @media screen and (min-width: ${breakpoints.medium}) {
     margin-right: ${sizes(5)};
   }
+  @media screen and (min-width: ${breakpoints.medium}) {
+    padding: 0;
+    margin-left: 2px;
+  }
 `
 
 export const ShortLogo = styled(UnstyledShortLogo)`
   display: block;
-  height: ${sizes(9)};
+  height: ${sizes(8)};
   @media screen and (min-width: ${breakpoints.medium}) {
     display: none;
   }
@@ -71,7 +76,7 @@ export const ShortLogo = styled(UnstyledShortLogo)`
 
 export const FullLogo = styled(UnstyledFullLogo)`
   display: none;
-  height: ${sizes(9)};
+  height: ${sizes(8)};
   @media screen and (min-width: ${breakpoints.medium}) {
     display: block;
   }
@@ -98,7 +103,7 @@ export const SearchbarContainer = styled.div`
   display: flex;
   justify-content: center;
   align-items: center;
-  margin-left: ${sizes(13)};
+  margin-left: ${sizes(14)};
 
   @media screen and (min-width: ${breakpoints.small}) {
     margin: 0;

+ 1 - 0
src/components/index.ts

@@ -12,3 +12,4 @@ export { default as ErrorFallback } from './ErrorFallback'
 export { default as ChannelLink } from './ChannelLink'
 export { default as BackgroundPattern } from './BackgroundPattern'
 export { InfiniteVideoGrid, InfiniteChannelGrid } from './InfiniteGrids'
+export { default as SideNavbar, SIDENAVBAR_WIDTH, EXPANDED_SIDENAVBAR_WIDTH, NavItem } from './SideNavbar'

+ 15 - 7
src/shared/components/HamburgerButton/HamburgerButton.style.ts

@@ -8,23 +8,31 @@ type HamburgerInnerProps = {
 }
 
 export const Hamburger = styled.button`
-  top: 19px;
+  height: ${sizes(11)};
+  width: ${sizes(11)};
+  padding: 0;
+  top: 18px;
   position: fixed;
   z-index: ${zIndex.header + 1};
   cursor: pointer;
-  background-color: ${colors.blue[700]};
+  background-color: ${colors.gray[700]};
   border: none;
+  border-radius: 100%;
   display: inline-block;
-  padding: ${sizes(3)};
   margin-left: ${sizes(3)};
   &:hover {
-    opacity: 0.7;
+    background-color: rgba(0, 0, 0, 0.12);
   }
-  @media screen and (min-width: ${breakpoints.small}) {
-    margin-left: ${sizes(3)};
+  &:focus {
+    background-color: rgba(0, 0, 0, 0.24);
+  }
+  &:active {
+    background-color: rgba(0, 0, 0, 0.4);
   }
   @media screen and (min-width: ${breakpoints.medium}) {
-    padding: ${sizes(2)};
+    top: 16px;
+    height: ${sizes(12)};
+    width: ${sizes(12)};
     background: none;
   }
 `

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

@@ -13,7 +13,6 @@ export { default as VideoPlayer } from './VideoPlayer'
 export { ChannelPreview, ChannelPreviewBase } from './ChannelPreview'
 export { default as HamburgerButton } from './HamburgerButton'
 export { default as Gallery } from './Gallery'
-export { default as SideNavbar, SIDENAVBAR_WIDTH, EXPANDED_SIDENAVBAR_WIDTH, NavItem } from './SideNavbar'
 export { default as ChannelAvatar } from './ChannelAvatar'
 export { default as GlobalStyle } from './GlobalStyle'
 export { default as ToggleButton } from './ToggleButton'

+ 4 - 8
src/views/LayoutWithRouting.tsx

@@ -2,31 +2,27 @@ import React, { useEffect } from 'react'
 import styled from '@emotion/styled'
 import { RouteComponentProps, Router, navigate, globalHistory } from '@reach/router'
 import { ErrorBoundary } from '@sentry/react'
-
-import { GlobalStyle, SideNavbar } from '@/shared/components'
-import { TopNavbar, ViewErrorFallback } from '@/components'
+import { GlobalStyle } from '@/shared/components'
+import { TopNavbar, ViewErrorFallback, SideNavbar } from '@/components'
 import { HomeView, VideoView, SearchView, ChannelView, VideosView, ChannelsView } from '@/views'
 import routes from '@/config/routes'
 import { globalStyles } from '@/styles/global'
 import { breakpoints, sizes } from '@/shared/theme'
-import { NavItemType } from '../shared/components/SideNavbar'
+import { NavItemType } from '@/components/SideNavbar'
 
 const SIDENAVBAR_ITEMS: NavItemType[] = [
   {
-    icon: 'home',
-    iconFilled: 'home-fill',
+    icon: 'home-fill',
     name: 'Home',
     to: routes.index(),
   },
   {
     icon: 'videos',
-    iconFilled: 'videos',
     name: 'Videos',
     to: routes.videos(),
   },
   {
     icon: 'channels',
-    iconFilled: 'channels',
     name: 'Channels',
     to: routes.channels(),
   },