[PART 17][Frontend] Create a Twitter clone with GraphQL, Typescript, and React ( Navbar )

[PART 17][Frontend] Create a Twitter clone with GraphQL, Typescript, and React ( Navbar )

Table of contents

Hi everyone ;).

As a reminder, I'm doing this Tweeter challenge

Github repository ( Backend )

Github repository ( Frontend )

Db diagram

Navbar

According to the design, there are two different navbars. One for the desktop and another one for mobile with a fixed menu at the bottom of the screen. I also have a condition to render the menu only if we have a connected user.

Desktop

Here is what my Navbar component looks like:

src/components/navbar/Navbar.tsx

import { Link } from 'react-router-dom'
import { useRecoilValue } from 'recoil'
import logo from '../../assets/tweeter.svg'
import { userState } from '../../state/userState'
import Menu from './Menu'
import UserMenu from './UserMenu'

const Navbar = () => {
  const user = useRecoilValue(userState)

  return (
    <div className="h-navbar border-b border-gray2 flex-none">
      <div className="w-full px-4 h-full flex items-center justify-between">
        <Link to="/">
          <img src={logo} alt="Logo Tweeter" />
        </Link>

        {user && (
          <>
            {/* Menu */}
            <Menu />
            {/* User menu */}
            <UserMenu />
          </>
        )}
      </div>
    </div>
  )
}

export default Navbar

I get the connected user from my recoil state.

The only "interesting" thing here is the UserMenu which has a custom drop-down menu.

src/components/navbar/UserMenu.tsx

import React, { useRef, useState } from 'react'
import { MdArrowDropDown } from 'react-icons/md'
import { useRecoilValue } from 'recoil'
import { useClickOutside } from '../../hooks/useClickOutside'
import { userState } from '../../state/userState'
import Avatar from '../Avatar'
import UserDropdown from './UserDropdown'

const UserMenu = () => {
  const [showDropdown, setShowDropdown] = useState(false)
  const user = useRecoilValue(userState)
  const menuRef = useRef(null)
  const dropdownRef = useRef(null)
  useClickOutside(dropdownRef, menuRef, () => {
    setShowDropdown(false)
  })
  return (
    <div
      ref={menuRef}
      className="flex items-center justify-center relative"
      onClick={() => setShowDropdown((old) => !old)}
    >
      <Avatar display_name={user?.display_name!} className="mr-3" />
      <div className="hidden cursor-pointer md:flex items-center">
        <div className="mr-4">{user?.display_name}</div>
        <MdArrowDropDown className="text-xl" />
      </div>
      <UserDropdown ref={dropdownRef} show={showDropdown} />
    </div>
  )
}

export default UserMenu

I created a custom hook to listen when a user clicks outside the drop-down.

src/hooks/useClickOutside.ts

import { exception } from 'console'
import { useEffect } from 'react'

export const useClickOutside = (
  ref: any,
  excludeRef: any,
  callback: Function
) => {
  useEffect(() => {
    const handleClickOutside = (event: any) => {
      if (ref.current && !ref.current.contains(event.target)) {
        if (excludeRef.current && !excludeRef.current.contains(event.target)) {
          callback()
        }
      }
    }
    // Bind the event listener
    document.addEventListener('mousedown', handleClickOutside)
    return () => {
      // Unbind the event listener on clean up
      document.removeEventListener('mousedown', handleClickOutside)
    }
  }, [ref])
}

The particularity here is the excludeRef property. I added it to allow to toggle the dropdown when clicking on the trigger ( the user's name ).

src/components/navbar/UserDropdown.tsx

import React, { forwardRef, useRef } from 'react'
import {
  MdAccountCircle,
  MdExitToApp,
  MdPeople,
  MdSettings,
} from 'react-icons/md'
import { Link } from 'react-router-dom'
import { useClickOutside } from '../../hooks/useClickOutside'
import UserDropdownLink from './UserDropdownLink'

type UserDropdownProps = {
  show: boolean
}

const UserDropdown = forwardRef(({ show }: UserDropdownProps, ref: any) => {
  return show ? (
    <div
      ref={ref}
      className="absolute top-0 right-0 mt-16 w-menuDropdown bg-white px-4 py-2 rounded-lg border border-gray6"
    >
      <div className="flex flex-col">
        <UserDropdownLink icon={<MdAccountCircle />} text="Profile" to="/" />
        <UserDropdownLink icon={<MdSettings />} text="Settings" to="/" />
      </div>
      <hr />
      <div>
        <UserDropdownLink
          icon={<MdExitToApp />}
          text="Logout"
          to="/"
          className="text-red-500"
        />
      </div>
    </div>
  ) : null
})

export default UserDropdown

Navbar Desktop

That's it for the desktop part. Let's take a look at the mobile version.

Mobile

For the mobile version, I have modified the Layout component a little bit.

src/components/Layout.tsx

import React from 'react'
import { useRecoilValue } from 'recoil'
import { userState } from '../state/userState'
import MenuMobile from './navbar/MenuMobile'
import Navbar from './navbar/Navbar'

type LayoutProps = {
  children: React.ReactNode
}

const Layout = ({ children }: LayoutProps) => {
  const user = useRecoilValue(userState)

  return (
    <div className="flex flex-col h-screen overflow-hidden md:h-full md:overflow-auto">
      <Navbar />
      <div className="w-full h-full overflow-y-auto md:overflow-y-visible">
        {children}
      </div>

      {/* Menu For Mobile */}
      {user && <MenuMobile />}
    </div>
  )
}

export default Layout

src/components/navbar/MenuMobile.tsx

import React from 'react'
import { MdBookmarkBorder, MdExplore, MdHome } from 'react-icons/md'

const MenuMobile = () => {
  return (
    <div className="md:hidden w-full h-16 bg-white z-10 flex flex-none items-center justify-around">
      <MdHome className="text-xl" />
      <MdExplore className="text-xl" />
      <MdBookmarkBorder className="text-xl" />
    </div>
  )
}

export default MenuMobile

Navbar Mobile

There is still a lot of work to be done, but I'm moving forward ;).

We will have a problem with the height:100vh even if it seems to work. I already had this issue on the Shoppingify Challenge. You can look here for a solution. This solution isn't perfect ( as it will re-render on every resize event ) and that's why I will not implement it right away ;).

That's all for today.

Bye and take care ;).