[PART 17][Frontend] Create a Twitter clone with GraphQL, Typescript, and React ( Navbar )
Hi everyone ;).
As a reminder, I'm doing this Tweeter challenge
Github repository ( Frontend )
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
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
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 ;).