Add left sidebar (with mobile support this time) (#71)
* Revert "Reverting side navbar for now"
This reverts commit a90441d9d5.
* Use padding instead of margin for bg color
* Use a slideout menu on mobile
* Remove "wide" page option
* Stick right sidebar on page bottom
* Darken bg on hover
			
			
This commit is contained in:
		
							parent
							
								
									a90441d9d5
								
							
						
					
					
						commit
						75e48204ef
					
				|  | @ -85,7 +85,7 @@ export default function FeedCreate(props: { | |||
|   return ( | ||||
|     <div | ||||
|       className={clsx( | ||||
|         'mt-2 w-full cursor-text rounded bg-white p-4 shadow-md', | ||||
|         'w-full cursor-text rounded bg-white p-4 shadow-md', | ||||
|         isExpanded ? 'ring-2 ring-indigo-300' : '', | ||||
|         className | ||||
|       )} | ||||
|  |  | |||
|  | @ -1,6 +1,6 @@ | |||
| import clsx from 'clsx' | ||||
| import { Fold } from '../../../common/fold' | ||||
| import { useFollowedFolds } from '../../hooks/use-fold' | ||||
| import { useFollowedFoldIds } from '../../hooks/use-fold' | ||||
| import { useUser } from '../../hooks/use-user' | ||||
| import { followFold, unfollowFold } from '../../lib/firebase/folds' | ||||
| 
 | ||||
|  | @ -9,7 +9,7 @@ export function FollowFoldButton(props: { fold: Fold; className?: string }) { | |||
| 
 | ||||
|   const user = useUser() | ||||
| 
 | ||||
|   const followedFoldIds = useFollowedFolds(user) | ||||
|   const followedFoldIds = useFollowedFoldIds(user) | ||||
|   const following = followedFoldIds | ||||
|     ? followedFoldIds.includes(fold.id) | ||||
|     : undefined | ||||
|  |  | |||
|  | @ -7,21 +7,24 @@ import { ENV_CONFIG } from '../../../common/envs/constants' | |||
| export function ManifoldLogo(props: { | ||||
|   className?: string | ||||
|   darkBackground?: boolean | ||||
|   hideText?: boolean | ||||
| }) { | ||||
|   const { darkBackground, className } = props | ||||
|   const { darkBackground, className, hideText } = props | ||||
| 
 | ||||
|   const user = useUser() | ||||
| 
 | ||||
|   return ( | ||||
|     <Link href={user ? '/home' : '/'}> | ||||
|       <a className={clsx('flex flex-shrink-0 flex-row gap-4', className)}> | ||||
|       <a className={clsx('group flex flex-shrink-0 flex-row gap-4', className)}> | ||||
|         <img | ||||
|           className="transition-all hover:rotate-12" | ||||
|           className="transition-all group-hover:rotate-12" | ||||
|           src={darkBackground ? '/logo-white.svg' : '/logo.svg'} | ||||
|           width={45} | ||||
|           height={45} | ||||
|         /> | ||||
|         {ENV_CONFIG.navbarLogoPath ? ( | ||||
| 
 | ||||
|         {!hideText && | ||||
|           (ENV_CONFIG.navbarLogoPath ? ( | ||||
|             <img src={ENV_CONFIG.navbarLogoPath} width={245} height={45} /> | ||||
|           ) : ( | ||||
|             <> | ||||
|  | @ -44,7 +47,7 @@ export function ManifoldLogo(props: { | |||
|                 Manifold Markets | ||||
|               </div> | ||||
|             </> | ||||
|         )} | ||||
|           ))} | ||||
|       </a> | ||||
|     </Link> | ||||
|   ) | ||||
|  |  | |||
|  | @ -11,7 +11,7 @@ export function MenuButton(props: { | |||
|   return ( | ||||
|     <Menu as="div" className={clsx('relative z-40 flex-shrink-0', className)}> | ||||
|       <div> | ||||
|         <Menu.Button className="flex rounded-full"> | ||||
|         <Menu.Button className="w-full rounded-full"> | ||||
|           <span className="sr-only">Open user menu</span> | ||||
|           {buttonContent} | ||||
|         </Menu.Button> | ||||
|  |  | |||
|  | @ -1,165 +1,128 @@ | |||
| import clsx from 'clsx' | ||||
| import Link from 'next/link' | ||||
| 
 | ||||
| import { useUser } from '../../hooks/use-user' | ||||
| import { Row } from '../layout/row' | ||||
| import { firebaseLogin, User } from '../../lib/firebase/users' | ||||
| import { ManifoldLogo } from './manifold-logo' | ||||
| import { ProfileMenu } from './profile-menu' | ||||
| import { | ||||
|   CollectionIcon, | ||||
|   HomeIcon, | ||||
|   MenuAlt3Icon, | ||||
|   SearchIcon, | ||||
|   UserGroupIcon, | ||||
|   XIcon, | ||||
| } from '@heroicons/react/outline' | ||||
| 
 | ||||
| export function NavBar(props: { | ||||
|   darkBackground?: boolean | ||||
|   wide?: boolean | ||||
|   assertUser?: 'signed-in' | 'signed-out' | ||||
|   className?: string | ||||
| }) { | ||||
|   const { darkBackground, wide, assertUser, className } = props | ||||
| 
 | ||||
|   const user = useUser() | ||||
| 
 | ||||
|   const hoverClasses = | ||||
|     'hover:underline hover:decoration-indigo-400 hover:decoration-2' | ||||
|   const themeClasses = clsx(darkBackground && 'text-white', hoverClasses) | ||||
| 
 | ||||
|   return ( | ||||
|     <> | ||||
|       <nav className={clsx('mb-4 w-full p-4', className)} aria-label="Global"> | ||||
|         <Row | ||||
|           className={clsx( | ||||
|             'mx-auto items-center justify-between sm:px-4', | ||||
|             wide ? 'max-w-6xl' : 'max-w-4xl' | ||||
|           )} | ||||
|         > | ||||
|           <ManifoldLogo className="my-1" darkBackground={darkBackground} /> | ||||
| 
 | ||||
|           <Row className="ml-6 items-center gap-6 sm:gap-8"> | ||||
|             {(user || user === null || assertUser) && ( | ||||
|               <NavOptions | ||||
|                 user={user} | ||||
|                 assertUser={assertUser} | ||||
|                 themeClasses={themeClasses} | ||||
|               /> | ||||
|             )} | ||||
|           </Row> | ||||
|         </Row> | ||||
|       </nav> | ||||
|       {user && <BottomNavBar user={user} />} | ||||
|     </> | ||||
|   ) | ||||
| } | ||||
| import { Transition, Dialog } from '@headlessui/react' | ||||
| import { useState, Fragment } from 'react' | ||||
| import Sidebar from './sidebar' | ||||
| 
 | ||||
| // From https://codepen.io/chris__sev/pen/QWGvYbL
 | ||||
| function BottomNavBar(props: { user: User }) { | ||||
|   const { user } = props | ||||
| export function BottomNavBar() { | ||||
|   const [sidebarOpen, setSidebarOpen] = useState(false) | ||||
|   const user = useUser() | ||||
|   if (!user) { | ||||
|     return null | ||||
|   } | ||||
|   return ( | ||||
|     <nav className="fixed inset-x-0 bottom-0 z-20 flex justify-between border-t-2 bg-white text-xs text-gray-700 md:hidden"> | ||||
|     <nav className="fixed inset-x-0 bottom-0 z-20 flex justify-between border-t-2 bg-white text-xs text-gray-700 lg:hidden"> | ||||
|       <Link href="/home"> | ||||
|         <a | ||||
|           href="#" | ||||
|           className="block w-full py-2 px-3 text-center transition duration-300 hover:bg-indigo-200 hover:text-indigo-700" | ||||
|         > | ||||
|         <a className="block w-full py-2 px-3 text-center transition duration-300 hover:bg-indigo-200 hover:text-indigo-700"> | ||||
|           <HomeIcon className="my-1 mx-auto h-6 w-6" aria-hidden="true" /> | ||||
|           {/* Home */} | ||||
|         </a> | ||||
|       </Link> | ||||
| 
 | ||||
|       <Link href="/markets"> | ||||
|         <a | ||||
|           href="#" | ||||
|           className="block w-full py-2 px-3 text-center hover:bg-indigo-200 hover:text-indigo-700" | ||||
|         > | ||||
|         <a className="block w-full py-2 px-3 text-center hover:bg-indigo-200 hover:text-indigo-700"> | ||||
|           <SearchIcon className="my-1 mx-auto h-6 w-6" aria-hidden="true" /> | ||||
|           {/* Explore */} | ||||
|         </a> | ||||
|       </Link> | ||||
| 
 | ||||
|       <Link href="/folds"> | ||||
|         <a | ||||
|           href="#" | ||||
|           className="block w-full py-2 px-3 text-center hover:bg-indigo-200 hover:text-indigo-700" | ||||
|         > | ||||
|         <a className="block w-full py-2 px-3 text-center hover:bg-indigo-200 hover:text-indigo-700"> | ||||
|           <UserGroupIcon className="my-1 mx-auto h-6 w-6" aria-hidden="true" /> | ||||
|           {/* Folds */} | ||||
|         </a> | ||||
|       </Link> | ||||
| 
 | ||||
|       <Link href="/trades"> | ||||
|         <a | ||||
|           href="#" | ||||
|           className="block w-full py-2 px-3 text-center hover:bg-indigo-200 hover:text-indigo-700" | ||||
|       <span | ||||
|         className="block w-full py-2 px-3 text-center hover:cursor-pointer hover:bg-indigo-200 hover:text-indigo-700" | ||||
|         onClick={() => setSidebarOpen(true)} | ||||
|       > | ||||
|           <CollectionIcon className="my-1 mx-auto h-6 w-6" aria-hidden="true" /> | ||||
|           {/* Your Trades */} | ||||
|         </a> | ||||
|       </Link> | ||||
|         <MenuAlt3Icon className="my-1 mx-auto h-6 w-6" aria-hidden="true" /> | ||||
|         {/* Menu */} | ||||
|       </span> | ||||
| 
 | ||||
|       <MobileSidebar | ||||
|         sidebarOpen={sidebarOpen} | ||||
|         setSidebarOpen={setSidebarOpen} | ||||
|       /> | ||||
|     </nav> | ||||
|   ) | ||||
| } | ||||
| 
 | ||||
| function NavOptions(props: { | ||||
|   user: User | null | undefined | ||||
|   assertUser: 'signed-in' | 'signed-out' | undefined | ||||
|   themeClasses: string | ||||
| // Sidebar that slides out on mobile
 | ||||
| export function MobileSidebar(props: { | ||||
|   sidebarOpen: boolean | ||||
|   setSidebarOpen: (open: boolean) => void | ||||
| }) { | ||||
|   const { user, assertUser, themeClasses } = props | ||||
|   const showSignedIn = assertUser === 'signed-in' || !!user | ||||
|   const showSignedOut = | ||||
|     !showSignedIn && (assertUser === 'signed-out' || user === null) | ||||
| 
 | ||||
|   const { sidebarOpen, setSidebarOpen } = props | ||||
|   return ( | ||||
|     <> | ||||
|       {showSignedOut && ( | ||||
|         <Link href="/about"> | ||||
|           <a | ||||
|             className={clsx( | ||||
|               'hidden whitespace-nowrap text-base md:block', | ||||
|               themeClasses | ||||
|             )} | ||||
|     <div> | ||||
|       <Transition.Root show={sidebarOpen} as={Fragment}> | ||||
|         <Dialog | ||||
|           as="div" | ||||
|           className="fixed inset-0 z-40 flex" | ||||
|           onClose={setSidebarOpen} | ||||
|         > | ||||
|             About | ||||
|           </a> | ||||
|         </Link> | ||||
|       )} | ||||
| 
 | ||||
|       <Link href="/folds"> | ||||
|         <a | ||||
|           className={clsx( | ||||
|             'hidden whitespace-nowrap text-base md:block', | ||||
|             themeClasses | ||||
|           )} | ||||
|           <Transition.Child | ||||
|             as={Fragment} | ||||
|             enter="transition-opacity ease-linear duration-300" | ||||
|             enterFrom="opacity-0" | ||||
|             enterTo="opacity-100" | ||||
|             leave="transition-opacity ease-linear duration-300" | ||||
|             leaveFrom="opacity-100" | ||||
|             leaveTo="opacity-0" | ||||
|           > | ||||
|           Communities | ||||
|         </a> | ||||
|       </Link> | ||||
| 
 | ||||
|       <Link href="/markets"> | ||||
|         <a | ||||
|           className={clsx( | ||||
|             'hidden whitespace-nowrap text-base md:block', | ||||
|             themeClasses | ||||
|           )} | ||||
|             <Dialog.Overlay className="fixed inset-0 bg-gray-600 bg-opacity-75" /> | ||||
|           </Transition.Child> | ||||
|           <Transition.Child | ||||
|             as={Fragment} | ||||
|             enter="transition ease-in-out duration-300 transform" | ||||
|             enterFrom="-translate-x-full" | ||||
|             enterTo="translate-x-0" | ||||
|             leave="transition ease-in-out duration-300 transform" | ||||
|             leaveFrom="translate-x-0" | ||||
|             leaveTo="-translate-x-full" | ||||
|           > | ||||
|           Markets | ||||
|         </a> | ||||
|       </Link> | ||||
| 
 | ||||
|       {showSignedOut && ( | ||||
|         <> | ||||
|             <div className="relative flex w-full max-w-xs flex-1 flex-col bg-white pt-5 pb-4"> | ||||
|               <Transition.Child | ||||
|                 as={Fragment} | ||||
|                 enter="ease-in-out duration-300" | ||||
|                 enterFrom="opacity-0" | ||||
|                 enterTo="opacity-100" | ||||
|                 leave="ease-in-out duration-300" | ||||
|                 leaveFrom="opacity-100" | ||||
|                 leaveTo="opacity-0" | ||||
|               > | ||||
|                 <div className="absolute top-0 right-0 -mr-12 pt-2"> | ||||
|                   <button | ||||
|             className="btn btn-sm btn-outline bg-gradient-to-r px-6 text-base font-medium normal-case" | ||||
|             onClick={firebaseLogin} | ||||
|                     type="button" | ||||
|                     className="ml-1 flex h-10 w-10 items-center justify-center rounded-full focus:outline-none focus:ring-2 focus:ring-inset focus:ring-white" | ||||
|                     onClick={() => setSidebarOpen(false)} | ||||
|                   > | ||||
|             Sign in | ||||
|                     <span className="sr-only">Close sidebar</span> | ||||
|                     <XIcon className="h-6 w-6 text-white" aria-hidden="true" /> | ||||
|                   </button> | ||||
|         </> | ||||
|       )} | ||||
|       {showSignedIn && <ProfileMenu user={user ?? undefined} />} | ||||
|     </> | ||||
|                 </div> | ||||
|               </Transition.Child> | ||||
|               <div className="mx-2 mt-5 h-0 flex-1 overflow-y-auto"> | ||||
|                 <Sidebar /> | ||||
|               </div> | ||||
|             </div> | ||||
|           </Transition.Child> | ||||
|           <div className="w-14 flex-shrink-0" aria-hidden="true"> | ||||
|             {/* Dummy element to force sidebar to shrink to fit close icon */} | ||||
|           </div> | ||||
|         </Dialog> | ||||
|       </Transition.Root> | ||||
|     </div> | ||||
|   ) | ||||
| } | ||||
|  |  | |||
|  | @ -1,106 +1,42 @@ | |||
| import { firebaseLogout, User } from '../../lib/firebase/users' | ||||
| import { formatMoney } from '../../../common/util/format' | ||||
| import { Avatar } from '../avatar' | ||||
| import { Col } from '../layout/col' | ||||
| import { MenuButton } from './menu' | ||||
| import { IS_PRIVATE_MANIFOLD } from '../../../common/envs/constants' | ||||
| import { Row } from '../layout/row' | ||||
| 
 | ||||
| export function ProfileMenu(props: { user: User | undefined }) { | ||||
|   const { user } = props | ||||
| 
 | ||||
|   return ( | ||||
|     <> | ||||
|       <MenuButton | ||||
|         className="hidden md:block" | ||||
|         menuItems={getNavigationOptions(user, { mobile: false })} | ||||
|         buttonContent={<ProfileSummary user={user} />} | ||||
|       /> | ||||
| 
 | ||||
|       <MenuButton | ||||
|         className="mr-2 md:hidden" | ||||
|         menuItems={getNavigationOptions(user, { mobile: true })} | ||||
|         buttonContent={<ProfileSummary user={user} />} | ||||
|       /> | ||||
|     </> | ||||
|   ) | ||||
| export function getNavigationOptions(user?: User | null) { | ||||
|   if (IS_PRIVATE_MANIFOLD) { | ||||
|     return [{ name: 'Leaderboards', href: '/leaderboards' }] | ||||
|   } | ||||
| 
 | ||||
| function getNavigationOptions( | ||||
|   user: User | undefined, | ||||
|   options: { mobile: boolean } | ||||
| ) { | ||||
|   const { mobile } = options | ||||
|   if (!user) { | ||||
|     return [ | ||||
|     { | ||||
|       name: 'Home', | ||||
|       href: user ? '/home' : '/', | ||||
|     }, | ||||
|     ...(mobile | ||||
|       ? [ | ||||
|           { | ||||
|             name: 'Markets', | ||||
|             href: '/markets', | ||||
|           }, | ||||
|           { | ||||
|             name: 'Communities', | ||||
|             href: '/folds', | ||||
|           }, | ||||
|         ] | ||||
|       : []), | ||||
|     { | ||||
|       name: `Your profile`, | ||||
|       href: `/${user?.username}`, | ||||
|     }, | ||||
|     { | ||||
|       name: 'Your trades', | ||||
|       href: '/trades', | ||||
|     }, | ||||
|     // Disable irrelevant menu options for teams.
 | ||||
|     ...(IS_PRIVATE_MANIFOLD | ||||
|       ? [ | ||||
|           { | ||||
|             name: 'Leaderboards', | ||||
|             href: '/leaderboards', | ||||
|           }, | ||||
|         ] | ||||
|       : [ | ||||
|           { | ||||
|             name: 'Add funds', | ||||
|             href: '/add-funds', | ||||
|           }, | ||||
|           { | ||||
|             name: 'Leaderboards', | ||||
|             href: '/leaderboards', | ||||
|           }, | ||||
|           { | ||||
|             name: 'Discord', | ||||
|             href: 'https://discord.gg/eHQBNBqXuh', | ||||
|           }, | ||||
|           { | ||||
|             name: 'About', | ||||
|             href: '/about', | ||||
|           }, | ||||
|         ]), | ||||
|     { | ||||
|       name: 'Sign out', | ||||
|       href: '#', | ||||
|       onClick: () => firebaseLogout(), | ||||
|     }, | ||||
|       { name: 'Leaderboards', href: '/leaderboards' }, | ||||
|       { name: 'Discord', href: 'https://discord.gg/eHQBNBqXuh' }, | ||||
|     ] | ||||
|   } | ||||
| 
 | ||||
| function ProfileSummary(props: { user: User | undefined }) { | ||||
|   return [ | ||||
|     { name: 'Your trades', href: '/trades' }, | ||||
|     { name: 'Add funds', href: '/add-funds' }, | ||||
|     { name: 'Leaderboards', href: '/leaderboards' }, | ||||
|     { name: 'Discord', href: 'https://discord.gg/eHQBNBqXuh' }, | ||||
|     { name: 'Sign out', href: '#', onClick: () => firebaseLogout() }, | ||||
|   ] | ||||
| } | ||||
| 
 | ||||
| export function ProfileSummary(props: { user: User | undefined }) { | ||||
|   const { user } = props | ||||
|   return ( | ||||
|     <Col className="avatar items-center gap-2 sm:flex-row sm:gap-4"> | ||||
|     <Row className="group avatar my-3 items-center gap-4 rounded-md py-3 text-gray-600 group-hover:bg-gray-100 group-hover:text-gray-900"> | ||||
|       <Avatar avatarUrl={user?.avatarUrl} username={user?.username} noLink /> | ||||
| 
 | ||||
|       <div className="truncate text-left sm:w-32"> | ||||
|         <div className="hidden sm:flex">{user?.name}</div> | ||||
|         <div className="text-sm text-gray-700"> | ||||
|       <div className="truncate text-left"> | ||||
|         <div>{user?.name}</div> | ||||
|         <div className="text-sm"> | ||||
|           {user ? formatMoney(Math.floor(user.balance)) : ' '} | ||||
|         </div> | ||||
|       </div> | ||||
|     </Col> | ||||
|     </Row> | ||||
|   ) | ||||
| } | ||||
|  |  | |||
							
								
								
									
										140
									
								
								web/components/nav/sidebar.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										140
									
								
								web/components/nav/sidebar.tsx
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,140 @@ | |||
| import { | ||||
|   HomeIcon, | ||||
|   UserGroupIcon, | ||||
|   SearchIcon, | ||||
|   BookOpenIcon, | ||||
|   DotsHorizontalIcon, | ||||
| } from '@heroicons/react/outline' | ||||
| import clsx from 'clsx' | ||||
| import _ from 'lodash' | ||||
| import Link from 'next/link' | ||||
| import { useRouter } from 'next/router' | ||||
| import { useFollowedFolds } from '../../hooks/use-fold' | ||||
| import { useUser } from '../../hooks/use-user' | ||||
| import { firebaseLogin } from '../../lib/firebase/users' | ||||
| import { ManifoldLogo } from './manifold-logo' | ||||
| import { MenuButton } from './menu' | ||||
| import { getNavigationOptions, ProfileSummary } from './profile-menu' | ||||
| 
 | ||||
| const navigation = [ | ||||
|   { name: 'Home', href: '/home', icon: HomeIcon }, | ||||
|   { name: 'Markets', href: '/markets', icon: SearchIcon }, | ||||
|   { name: 'About', href: 'https://docs.manifold.markets', icon: BookOpenIcon }, | ||||
| ] | ||||
| 
 | ||||
| type Item = { | ||||
|   name: string | ||||
|   href: string | ||||
|   icon: React.ComponentType<{ className?: string }> | ||||
| } | ||||
| 
 | ||||
| function SidebarItem(props: { item: Item; currentPage: string }) { | ||||
|   const { item, currentPage } = props | ||||
|   return ( | ||||
|     <Link href={item.href} key={item.name}> | ||||
|       <a | ||||
|         className={clsx( | ||||
|           item.href == currentPage | ||||
|             ? 'bg-gray-200 text-gray-900' | ||||
|             : 'text-gray-600 hover:bg-gray-100', | ||||
|           'group flex items-center rounded-md px-3 py-2 text-sm font-medium' | ||||
|         )} | ||||
|         aria-current={item.href == currentPage ? 'page' : undefined} | ||||
|       > | ||||
|         <item.icon | ||||
|           className={clsx( | ||||
|             item.href == currentPage | ||||
|               ? 'text-gray-500' | ||||
|               : 'text-gray-400 group-hover:text-gray-500', | ||||
|             '-ml-1 mr-3 h-6 w-6 flex-shrink-0' | ||||
|           )} | ||||
|           aria-hidden="true" | ||||
|         /> | ||||
|         <span className="truncate">{item.name}</span> | ||||
|       </a> | ||||
|     </Link> | ||||
|   ) | ||||
| } | ||||
| 
 | ||||
| function MoreButton() { | ||||
|   return ( | ||||
|     <a className="group flex items-center rounded-md px-3 py-2 text-sm font-medium text-gray-600 hover:cursor-pointer hover:bg-gray-100"> | ||||
|       <DotsHorizontalIcon | ||||
|         className="-ml-1 mr-3 h-6 w-6 flex-shrink-0 text-gray-400 group-hover:text-gray-500" | ||||
|         aria-hidden="true" | ||||
|       /> | ||||
|       <span className="truncate">More</span> | ||||
|     </a> | ||||
|   ) | ||||
| } | ||||
| 
 | ||||
| export default function Sidebar() { | ||||
|   const router = useRouter() | ||||
|   const currentPage = router.pathname | ||||
| 
 | ||||
|   const user = useUser() | ||||
|   let folds = useFollowedFolds(user) || [] | ||||
|   folds = _.sortBy(folds, 'followCount').reverse() | ||||
| 
 | ||||
|   return ( | ||||
|     <nav | ||||
|       aria-label="Sidebar" | ||||
|       className="sticky top-4 mt-4 divide-y divide-gray-300" | ||||
|     > | ||||
|       <div className="space-y-1 pb-6"> | ||||
|         <ManifoldLogo hideText /> | ||||
|       </div> | ||||
| 
 | ||||
|       <div style={{ minHeight: 93 }}> | ||||
|         {user ? ( | ||||
|           <Link href={`/${user.username}`}> | ||||
|             <a className="group"> | ||||
|               <ProfileSummary user={user} /> | ||||
|             </a> | ||||
|           </Link> | ||||
|         ) : user === null ? ( | ||||
|           <div className="py-6 text-center"> | ||||
|             <button | ||||
|               className="btn border-none bg-gradient-to-r from-teal-500 to-green-500 px-10 text-lg font-medium normal-case hover:from-teal-600 hover:to-green-600" | ||||
|               onClick={firebaseLogin} | ||||
|             > | ||||
|               Sign in | ||||
|             </button> | ||||
|           </div> | ||||
|         ) : ( | ||||
|           <div /> | ||||
|         )} | ||||
|       </div> | ||||
| 
 | ||||
|       <div className="space-y-1 py-6"> | ||||
|         {navigation.map((item) => ( | ||||
|           <SidebarItem item={item} currentPage={currentPage} /> | ||||
|         ))} | ||||
| 
 | ||||
|         <MenuButton | ||||
|           menuItems={getNavigationOptions(user)} | ||||
|           buttonContent={<MoreButton />} | ||||
|         /> | ||||
|       </div> | ||||
| 
 | ||||
|       <div className="pt-6"> | ||||
|         <SidebarItem | ||||
|           item={{ name: 'Communities', href: '/folds', icon: UserGroupIcon }} | ||||
|           currentPage={currentPage} | ||||
|         /> | ||||
| 
 | ||||
|         <div className="mt-3 space-y-2"> | ||||
|           {folds.map((fold) => ( | ||||
|             <a | ||||
|               key={fold.name} | ||||
|               href={`/fold/${fold.slug}`} | ||||
|               className="group flex items-center rounded-md px-3 py-2 text-sm font-medium text-gray-600 hover:bg-gray-100 hover:text-gray-900" | ||||
|             > | ||||
|               <span className="truncate">  {fold.name}</span> | ||||
|             </a> | ||||
|           ))} | ||||
|         </div> | ||||
|       </div> | ||||
|     </nav> | ||||
|   ) | ||||
| } | ||||
|  | @ -1,27 +1,43 @@ | |||
| import clsx from 'clsx' | ||||
| import { NavBar } from './nav/nav-bar' | ||||
| import { BottomNavBar } from './nav/nav-bar' | ||||
| import Sidebar from './nav/sidebar' | ||||
| 
 | ||||
| export function Page(props: { | ||||
|   wide?: boolean | ||||
|   margin?: boolean | ||||
|   assertUser?: 'signed-in' | 'signed-out' | ||||
|   rightSidebar?: React.ReactNode | ||||
|   children?: any | ||||
| }) { | ||||
|   const { wide, margin, assertUser, children } = props | ||||
|   const { margin, assertUser, children, rightSidebar } = props | ||||
| 
 | ||||
|   return ( | ||||
|     <div> | ||||
|       <NavBar wide={wide} assertUser={assertUser} /> | ||||
| 
 | ||||
|       <div | ||||
|         className={clsx( | ||||
|           'mx-auto w-full pb-16', | ||||
|           wide ? 'max-w-6xl' : 'max-w-4xl', | ||||
|           'mx-auto w-full pb-16 lg:grid lg:grid-cols-12 lg:gap-8 xl:max-w-7xl', | ||||
|           margin && 'px-4' | ||||
|         )} | ||||
|       > | ||||
|         {children} | ||||
|         <div className="hidden lg:col-span-3 lg:block xl:col-span-2"> | ||||
|           {assertUser !== 'signed-out' && <Sidebar />} | ||||
|         </div> | ||||
|         <main | ||||
|           className={clsx( | ||||
|             'pt-6 lg:col-span-9', | ||||
|             rightSidebar ? 'xl:col-span-7' : 'xl:col-span-8' | ||||
|           )} | ||||
|         > | ||||
|           {children} | ||||
| 
 | ||||
|           {/* If right sidebar is hidden, place its content at the bottom of the page. */} | ||||
|           <div className="block xl:hidden">{rightSidebar}</div> | ||||
|         </main> | ||||
|         <aside className="hidden xl:col-span-3 xl:block"> | ||||
|           <div className="sticky top-4 space-y-4">{rightSidebar}</div> | ||||
|         </aside> | ||||
|       </div> | ||||
| 
 | ||||
|       <BottomNavBar /> | ||||
|     </div> | ||||
|   ) | ||||
| } | ||||
|  |  | |||
|  | @ -72,7 +72,7 @@ export function ResolutionPanel(props: { | |||
| 
 | ||||
|   return ( | ||||
|     <Col className={clsx('rounded-md bg-white px-8 py-6', className)}> | ||||
|       <Title className="mt-0 whitespace-nowrap" text="Resolve market" /> | ||||
|       <Title className="!mt-0 whitespace-nowrap" text="Resolve market" /> | ||||
| 
 | ||||
|       <div className="mb-2 text-sm text-gray-500">Outcome</div> | ||||
| 
 | ||||
|  |  | |||
|  | @ -10,7 +10,7 @@ import { Comment, getRecentComments } from '../lib/firebase/comments' | |||
| import { Contract, getActiveContracts } from '../lib/firebase/contracts' | ||||
| import { listAllFolds } from '../lib/firebase/folds' | ||||
| import { useInactiveContracts } from './use-contracts' | ||||
| import { useFollowedFolds } from './use-fold' | ||||
| import { useFollowedFoldIds } from './use-fold' | ||||
| import { useSeenContracts } from './use-seen-contracts' | ||||
| import { useUserBetContracts } from './use-user-bets' | ||||
| 
 | ||||
|  | @ -48,7 +48,7 @@ export const useFilterYourContracts = ( | |||
|   folds: Fold[], | ||||
|   contracts: Contract[] | ||||
| ) => { | ||||
|   const followedFoldIds = useFollowedFolds(user) | ||||
|   const followedFoldIds = useFollowedFoldIds(user) | ||||
| 
 | ||||
|   const followedFolds = filterDefined( | ||||
|     (followedFoldIds ?? []).map((id) => folds.find((fold) => fold.id === id)) | ||||
|  |  | |||
|  | @ -1,7 +1,9 @@ | |||
| import _ from 'lodash' | ||||
| import { useEffect, useState } from 'react' | ||||
| import { Fold } from '../../common/fold' | ||||
| import { User } from '../../common/user' | ||||
| import { | ||||
|   listAllFolds, | ||||
|   listenForFold, | ||||
|   listenForFolds, | ||||
|   listenForFoldsWithTags, | ||||
|  | @ -49,8 +51,8 @@ export const useFollowingFold = (fold: Fold, user: User | null | undefined) => { | |||
|   return following | ||||
| } | ||||
| 
 | ||||
| // Note: We cache FollowedFolds in localstorage to speed up the initial load
 | ||||
| export const useFollowedFolds = (user: User | null | undefined) => { | ||||
| // Note: We cache followedFoldIds in localstorage to speed up the initial load
 | ||||
| export const useFollowedFoldIds = (user: User | null | undefined) => { | ||||
|   const [followedFoldIds, setFollowedFoldIds] = useState<string[] | undefined>( | ||||
|     undefined | ||||
|   ) | ||||
|  | @ -72,3 +74,38 @@ export const useFollowedFolds = (user: User | null | undefined) => { | |||
| 
 | ||||
|   return followedFoldIds | ||||
| } | ||||
| 
 | ||||
| // We also cache followedFolds directly in JSON.
 | ||||
| // TODO: Extract out localStorage caches to a utility
 | ||||
| export const useFollowedFolds = (user: User | null | undefined) => { | ||||
|   const [followedFolds, setFollowedFolds] = useState<Fold[] | undefined>() | ||||
|   const ids = useFollowedFoldIds(user) | ||||
| 
 | ||||
|   useEffect(() => { | ||||
|     if (user && ids) { | ||||
|       const key = `followed-full-folds-${user.id}` | ||||
|       const followedFoldJson = localStorage.getItem(key) | ||||
|       if (followedFoldJson) { | ||||
|         setFollowedFolds(JSON.parse(followedFoldJson)) | ||||
|         // Exit early if ids and followedFoldIds have all the same elements.
 | ||||
|         if ( | ||||
|           _.isEqual( | ||||
|             _.sortBy(ids), | ||||
|             _.sortBy(JSON.parse(followedFoldJson).map((f: Fold) => f.id)) | ||||
|           ) | ||||
|         ) { | ||||
|           return | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
|       // Otherwise, fetch the full contents of all folds
 | ||||
|       listAllFolds().then((folds) => { | ||||
|         const followedFolds = folds.filter((fold) => ids.includes(fold.id)) | ||||
|         setFollowedFolds(followedFolds) | ||||
|         localStorage.setItem(key, JSON.stringify(followedFolds)) | ||||
|       }) | ||||
|     } | ||||
|   }, [user, ids]) | ||||
| 
 | ||||
|   return followedFolds | ||||
| } | ||||
|  |  | |||
|  | @ -121,8 +121,17 @@ export default function ContractPage(props: { | |||
| 
 | ||||
|   const ogCardProps = getOpenGraphProps(contract) | ||||
| 
 | ||||
|   const rightSidebar = hasSidePanel ? ( | ||||
|     <Col className="gap-4"> | ||||
|       {allowTrade && ( | ||||
|         <BetPanel className="hidden lg:flex" contract={contract} /> | ||||
|       )} | ||||
|       {allowResolve && <ResolutionPanel creator={user} contract={contract} />} | ||||
|     </Col> | ||||
|   ) : null | ||||
| 
 | ||||
|   return ( | ||||
|     <Page wide={hasSidePanel}> | ||||
|     <Page rightSidebar={rightSidebar}> | ||||
|       {ogCardProps && ( | ||||
|         <SEO | ||||
|           title={question} | ||||
|  | @ -168,21 +177,6 @@ export default function ContractPage(props: { | |||
|           )} | ||||
|           <BetsSection contract={contract} user={user ?? null} bets={bets} /> | ||||
|         </div> | ||||
| 
 | ||||
|         {hasSidePanel && ( | ||||
|           <> | ||||
|             <div className="md:ml-6" /> | ||||
| 
 | ||||
|             <Col className="flex-shrink-0 md:w-[310px]"> | ||||
|               {allowTrade && ( | ||||
|                 <BetPanel className="hidden lg:flex" contract={contract} /> | ||||
|               )} | ||||
|               {allowResolve && ( | ||||
|                 <ResolutionPanel creator={user} contract={contract} /> | ||||
|               )} | ||||
|             </Col> | ||||
|           </> | ||||
|         )} | ||||
|       </Col> | ||||
|     </Page> | ||||
|   ) | ||||
|  |  | |||
|  | @ -205,7 +205,7 @@ function ContractsTable() { | |||
| 
 | ||||
| export default function Admin() { | ||||
|   return useAdmin() ? ( | ||||
|     <Page wide> | ||||
|     <Page> | ||||
|       <UsersTable /> | ||||
|       <ContractsTable /> | ||||
|     </Page> | ||||
|  |  | |||
|  | @ -159,8 +159,28 @@ export default function FoldPage(props: { | |||
|     return <Custom404 /> | ||||
|   } | ||||
| 
 | ||||
|   const rightSidebar = ( | ||||
|     <Col className="mt-6 gap-12"> | ||||
|       <Row className="justify-end"> | ||||
|         {isCurator ? ( | ||||
|           <EditFoldButton className="ml-1" fold={fold} /> | ||||
|         ) : ( | ||||
|           <FollowFoldButton className="ml-1" fold={fold} /> | ||||
|         )} | ||||
|       </Row> | ||||
|       <FoldOverview fold={fold} curator={curator} /> | ||||
|       <FoldLeaderboards | ||||
|         traderScores={traderScores} | ||||
|         creatorScores={creatorScores} | ||||
|         topTraders={topTraders} | ||||
|         topCreators={topCreators} | ||||
|         user={user} | ||||
|       /> | ||||
|     </Col> | ||||
|   ) | ||||
| 
 | ||||
|   return ( | ||||
|     <Page wide> | ||||
|     <Page rightSidebar={rightSidebar}> | ||||
|       <SEO | ||||
|         title={fold.name} | ||||
|         description={`Curated by ${curator.name}. ${fold.about}`} | ||||
|  | @ -170,11 +190,6 @@ export default function FoldPage(props: { | |||
|       <div className="px-3 lg:px-1"> | ||||
|         <Row className="mb-6 justify-between"> | ||||
|           <Title className="!m-0" text={fold.name} /> | ||||
|           {isCurator ? ( | ||||
|             <EditFoldButton className="ml-1" fold={fold} /> | ||||
|           ) : ( | ||||
|             <FollowFoldButton className="ml-1" fold={fold} /> | ||||
|           )} | ||||
|         </Row> | ||||
| 
 | ||||
|         <Col className="mb-6 gap-2 text-gray-500 md:hidden"> | ||||
|  | @ -259,16 +274,6 @@ export default function FoldPage(props: { | |||
|               <SearchableGrid contracts={contracts} /> | ||||
|             )} | ||||
|           </Col> | ||||
|           <Col className="hidden w-full max-w-xs gap-12 md:flex"> | ||||
|             <FoldOverview fold={fold} curator={curator} /> | ||||
|             <FoldLeaderboards | ||||
|               traderScores={traderScores} | ||||
|               creatorScores={creatorScores} | ||||
|               topTraders={topTraders} | ||||
|               topCreators={topCreators} | ||||
|               user={user} | ||||
|             /> | ||||
|           </Col> | ||||
|         </Row> | ||||
|       )} | ||||
| 
 | ||||
|  |  | |||
|  | @ -7,11 +7,10 @@ import { FollowFoldButton } from '../components/folds/follow-fold-button' | |||
| import { Col } from '../components/layout/col' | ||||
| import { Row } from '../components/layout/row' | ||||
| import { Page } from '../components/page' | ||||
| import { SiteLink } from '../components/site-link' | ||||
| import { TagsList } from '../components/tags-list' | ||||
| import { Title } from '../components/title' | ||||
| import { UserLink } from '../components/user-page' | ||||
| import { useFolds, useFollowedFolds } from '../hooks/use-fold' | ||||
| import { useFolds, useFollowedFoldIds } from '../hooks/use-fold' | ||||
| import { useUser } from '../hooks/use-user' | ||||
| import { foldPath, listAllFolds } from '../lib/firebase/folds' | ||||
| import { getUser, User } from '../lib/firebase/users' | ||||
|  | @ -44,7 +43,7 @@ export default function Folds(props: { | |||
| 
 | ||||
|   let folds = useFolds() ?? props.folds | ||||
|   const user = useUser() | ||||
|   const followedFoldIds = useFollowedFolds(user) || [] | ||||
|   const followedFoldIds = useFollowedFoldIds(user) || [] | ||||
|   // First sort by follower count, then list followed folds first
 | ||||
|   folds = _.sortBy(folds, (fold) => -1 * fold.followCount) | ||||
|   folds = _.sortBy(folds, (fold) => !followedFoldIds.includes(fold.id)) | ||||
|  |  | |||
|  | @ -10,7 +10,6 @@ import { | |||
| import { firebaseLogin } from '../lib/firebase/users' | ||||
| import { ContractsGrid } from '../components/contracts-list' | ||||
| import { Col } from '../components/layout/col' | ||||
| import { NavBar } from '../components/nav/nav-bar' | ||||
| import Link from 'next/link' | ||||
| import { Contract } from '../lib/firebase/contracts' | ||||
| 
 | ||||
|  | @ -34,7 +33,6 @@ const scrollToAbout = () => { | |||
| function Hero() { | ||||
|   return ( | ||||
|     <div className="bg-world-trading h-screen overflow-hidden bg-gray-900 bg-cover bg-center lg:bg-left"> | ||||
|       <NavBar darkBackground /> | ||||
|       <main> | ||||
|         <div className="pt-32 sm:pt-8 lg:overflow-hidden lg:pt-0 lg:pb-14"> | ||||
|           <div className="mx-auto max-w-7xl lg:px-8 xl:px-0"> | ||||
|  |  | |||
|  | @ -3,7 +3,6 @@ import { DatumValue } from '@nivo/core' | |||
| import { ResponsiveLine } from '@nivo/line' | ||||
| 
 | ||||
| import { Entry, makeEntries } from '../lib/simulator/entries' | ||||
| import { NavBar } from '../components/nav/nav-bar' | ||||
| import { Col } from '../components/layout/col' | ||||
| 
 | ||||
| function TableBody(props: { entries: Entry[] }) { | ||||
|  | @ -254,7 +253,6 @@ export default function Simulator() { | |||
| 
 | ||||
|   return ( | ||||
|     <Col> | ||||
|       <NavBar /> | ||||
|       <div className="mx-auto mt-8 grid w-full grid-cols-1 gap-4 p-2 text-center xl:grid-cols-2"> | ||||
|         {/* Left column */} | ||||
|         <div> | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue
	
	Block a user