Hey is a decentralized and permissionless social media app built with Lens Protocol 馃尶
at main 205 lines 5.8 kB view raw
1import { useApolloClient } from "@apollo/client"; 2import { 3 BellIcon as BellOutline, 4 BookmarkIcon as BookmarkOutline, 5 GlobeAltIcon as GlobeOutline, 6 HomeIcon as HomeOutline, 7 UserCircleIcon, 8 UserGroupIcon as UserGroupOutline 9} from "@heroicons/react/24/outline"; 10import { 11 BellIcon as BellSolid, 12 BookmarkIcon as BookmarkSolid, 13 GlobeAltIcon as GlobeSolid, 14 HomeIcon as HomeSolid, 15 UserGroupIcon as UserGroupSolid 16} from "@heroicons/react/24/solid"; 17import { STATIC_IMAGES_URL } from "@hey/data/constants"; 18import { 19 GroupsDocument, 20 NotificationIndicatorDocument, 21 NotificationsDocument, 22 PostBookmarksDocument, 23 PostsExploreDocument, 24 PostsForYouDocument, 25 TimelineDocument, 26 TimelineHighlightsDocument 27} from "@hey/indexer"; 28import { 29 type MouseEvent, 30 memo, 31 type ReactNode, 32 useCallback, 33 useState 34} from "react"; 35import { Link, useLocation } from "react-router"; 36import Pro from "@/components/Shared/Navbar/NavItems/Pro"; 37import { Image, Spinner, Tooltip } from "@/components/Shared/UI"; 38import useHasNewNotifications from "@/hooks/useHasNewNotifications"; 39import { useAuthModalStore } from "@/store/non-persisted/modal/useAuthModalStore"; 40import { useAccountStore } from "@/store/persisted/useAccountStore"; 41import SignedAccount from "./SignedAccount"; 42 43const navigationItems = { 44 "/": { 45 outline: <HomeOutline className="size-6" />, 46 refreshDocs: [ 47 TimelineDocument, 48 TimelineHighlightsDocument, 49 PostsForYouDocument 50 ], 51 solid: <HomeSolid className="size-6" />, 52 title: "Home" 53 }, 54 "/bookmarks": { 55 outline: <BookmarkOutline className="size-6" />, 56 refreshDocs: [PostBookmarksDocument], 57 solid: <BookmarkSolid className="size-6" />, 58 title: "Bookmarks" 59 }, 60 "/explore": { 61 outline: <GlobeOutline className="size-6" />, 62 refreshDocs: [PostsExploreDocument], 63 solid: <GlobeSolid className="size-6" />, 64 title: "Explore" 65 }, 66 "/groups": { 67 outline: <UserGroupOutline className="size-6" />, 68 refreshDocs: [GroupsDocument], 69 solid: <UserGroupSolid className="size-6" />, 70 title: "Groups" 71 }, 72 "/notifications": { 73 outline: <BellOutline className="size-6" />, 74 refreshDocs: [NotificationsDocument, NotificationIndicatorDocument], 75 solid: <BellSolid className="size-6" />, 76 title: "Notifications" 77 } 78}; 79 80interface NavItemProps { 81 url: string; 82 icon: ReactNode; 83 onClick?: (e: MouseEvent<HTMLAnchorElement>) => void; 84} 85 86const NavItem = memo(({ icon, onClick, url }: NavItemProps) => ( 87 <Tooltip content={navigationItems[url as keyof typeof navigationItems].title}> 88 <Link onClick={onClick} to={url}> 89 {icon} 90 </Link> 91 </Tooltip> 92)); 93 94const NavItems = memo(({ isLoggedIn }: { isLoggedIn: boolean }) => { 95 const { pathname } = useLocation(); 96 const hasNewNotifications = useHasNewNotifications(); 97 const client = useApolloClient(); 98 const [refreshingRoute, setRefreshingRoute] = useState<string | null>(null); 99 const routes = [ 100 "/", 101 "/explore", 102 ...(isLoggedIn ? ["/notifications", "/groups", "/bookmarks"] : []) 103 ]; 104 105 return ( 106 <> 107 {routes.map((route) => { 108 let icon = 109 pathname === route 110 ? navigationItems[route as keyof typeof navigationItems].solid 111 : navigationItems[route as keyof typeof navigationItems].outline; 112 113 if (refreshingRoute === route) { 114 icon = <Spinner className="my-0.5" size="sm" />; 115 } 116 117 const iconWithIndicator = 118 route === "/notifications" ? ( 119 <span className="relative"> 120 {icon} 121 {hasNewNotifications && ( 122 <span className="-right-1 -top-1 absolute size-2 rounded-full bg-red-500" /> 123 )} 124 </span> 125 ) : ( 126 icon 127 ); 128 129 const handleClick = async (e: MouseEvent<HTMLAnchorElement>) => { 130 const item = navigationItems[route as keyof typeof navigationItems]; 131 const isSameRoute = pathname === route; 132 if (!isSameRoute || !("refreshDocs" in item) || !item.refreshDocs) { 133 return; 134 } 135 e.preventDefault(); 136 window.scrollTo(0, 0); 137 setRefreshingRoute(route); 138 try { 139 await client.refetchQueries({ include: item.refreshDocs }); 140 } finally { 141 setRefreshingRoute(null); 142 } 143 }; 144 145 return ( 146 <NavItem 147 icon={iconWithIndicator} 148 key={route} 149 onClick={handleClick} 150 url={route} 151 /> 152 ); 153 })} 154 </> 155 ); 156}); 157 158const Navbar = () => { 159 const { pathname } = useLocation(); 160 const { currentAccount } = useAccountStore(); 161 const { setShowAuthModal } = useAuthModalStore(); 162 163 const handleLogoClick = useCallback( 164 (e: MouseEvent<HTMLAnchorElement>) => { 165 if (pathname === "/") { 166 e.preventDefault(); 167 window.scrollTo(0, 0); 168 } 169 }, 170 [pathname] 171 ); 172 173 const handleAuthClick = useCallback(() => { 174 setShowAuthModal(true); 175 }, []); 176 177 return ( 178 <aside className="sticky top-5 mt-5 hidden w-10 shrink-0 flex-col items-center gap-y-5 md:flex"> 179 <Link onClick={handleLogoClick} to="/"> 180 <Image 181 alt="Logo" 182 className="size-8" 183 height={32} 184 src={`${STATIC_IMAGES_URL}/app-icon/0.png`} 185 width={32} 186 /> 187 </Link> 188 <NavItems isLoggedIn={!!currentAccount} /> 189 {currentAccount ? ( 190 <> 191 <Pro /> 192 <SignedAccount /> 193 </> 194 ) : ( 195 <button onClick={handleAuthClick} type="button"> 196 <Tooltip content="Login"> 197 <UserCircleIcon className="size-6" /> 198 </Tooltip> 199 </button> 200 )} 201 </aside> 202 ); 203}; 204 205export default memo(Navbar);