diff --git a/web/components/feed-create.tsx b/web/components/feed-create.tsx index de4d6e22..46fd99c5 100644 --- a/web/components/feed-create.tsx +++ b/web/components/feed-create.tsx @@ -85,7 +85,7 @@ export default function FeedCreate(props: { return (
- + - {ENV_CONFIG.navbarLogoPath ? ( - - ) : ( - <> -
- Manifold -
- Markets -
- - - )} + + {!hideText && + (ENV_CONFIG.navbarLogoPath ? ( + + ) : ( + <> +
+ Manifold +
+ Markets +
+ + + ))}
) diff --git a/web/components/nav/menu.tsx b/web/components/nav/menu.tsx index a0345e37..9d348fab 100644 --- a/web/components/nav/menu.tsx +++ b/web/components/nav/menu.tsx @@ -11,7 +11,7 @@ export function MenuButton(props: { return (
- + Open user menu {buttonContent} diff --git a/web/components/nav/nav-bar.tsx b/web/components/nav/nav-bar.tsx index f600d994..784d1129 100644 --- a/web/components/nav/nav-bar.tsx +++ b/web/components/nav/nav-bar.tsx @@ -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 ( - <> - - {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 ( -
) } diff --git a/web/components/nav/profile-menu.tsx b/web/components/nav/profile-menu.tsx index 98de7286..990c12bc 100644 --- a/web/components/nav/profile-menu.tsx +++ b/web/components/nav/profile-menu.tsx @@ -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 +export function getNavigationOptions(user?: User | null) { + if (IS_PRIVATE_MANIFOLD) { + return [{ name: 'Leaderboards', href: '/leaderboards' }] + } - return ( - <> - } - /> + if (!user) { + return [ + { name: 'Leaderboards', href: '/leaderboards' }, + { name: 'Discord', href: 'https://discord.gg/eHQBNBqXuh' }, + ] + } - } - /> - - ) -} - -function getNavigationOptions( - user: User | undefined, - options: { mobile: boolean } -) { - const { mobile } = options 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: '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() }, ] } -function ProfileSummary(props: { user: User | undefined }) { +export function ProfileSummary(props: { user: User | undefined }) { const { user } = props return ( - + -
-
{user?.name}
-
+
+
{user?.name}
+
{user ? formatMoney(Math.floor(user.balance)) : ' '}
- + ) } diff --git a/web/components/nav/sidebar.tsx b/web/components/nav/sidebar.tsx new file mode 100644 index 00000000..cb7055df --- /dev/null +++ b/web/components/nav/sidebar.tsx @@ -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 ( + + + + + ) +} + +function MoreButton() { + return ( + + + ) +} + +export default function Sidebar() { + const router = useRouter() + const currentPage = router.pathname + + const user = useUser() + let folds = useFollowedFolds(user) || [] + folds = _.sortBy(folds, 'followCount').reverse() + + return ( + + ) +} diff --git a/web/components/page.tsx b/web/components/page.tsx index 95ff4270..61ce4a47 100644 --- a/web/components/page.tsx +++ b/web/components/page.tsx @@ -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 (
- -
- {children} +
+ {assertUser !== 'signed-out' && } +
+
+ {children} + + {/* If right sidebar is hidden, place its content at the bottom of the page. */} +
{rightSidebar}
+
+
+ +
) } diff --git a/web/components/resolution-panel.tsx b/web/components/resolution-panel.tsx index b22a45e4..2469c8f1 100644 --- a/web/components/resolution-panel.tsx +++ b/web/components/resolution-panel.tsx @@ -72,7 +72,7 @@ export function ResolutionPanel(props: { return ( - + <Title className="!mt-0 whitespace-nowrap" text="Resolve market" /> <div className="mb-2 text-sm text-gray-500">Outcome</div> diff --git a/web/hooks/use-find-active-contracts.ts b/web/hooks/use-find-active-contracts.ts index ba80a899..66573728 100644 --- a/web/hooks/use-find-active-contracts.ts +++ b/web/hooks/use-find-active-contracts.ts @@ -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)) diff --git a/web/hooks/use-fold.ts b/web/hooks/use-fold.ts index e7eb737f..ffb19615 100644 --- a/web/hooks/use-fold.ts +++ b/web/hooks/use-fold.ts @@ -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 +} diff --git a/web/pages/[username]/[contractSlug].tsx b/web/pages/[username]/[contractSlug].tsx index 1c321abc..ee0ef26a 100644 --- a/web/pages/[username]/[contractSlug].tsx +++ b/web/pages/[username]/[contractSlug].tsx @@ -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> ) diff --git a/web/pages/admin.tsx b/web/pages/admin.tsx index 637e2801..e71e1cf1 100644 --- a/web/pages/admin.tsx +++ b/web/pages/admin.tsx @@ -205,7 +205,7 @@ function ContractsTable() { export default function Admin() { return useAdmin() ? ( - <Page wide> + <Page> <UsersTable /> <ContractsTable /> </Page> diff --git a/web/pages/fold/[...slugs]/index.tsx b/web/pages/fold/[...slugs]/index.tsx index c7ac014a..01c4a471 100644 --- a/web/pages/fold/[...slugs]/index.tsx +++ b/web/pages/fold/[...slugs]/index.tsx @@ -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> )} diff --git a/web/pages/folds.tsx b/web/pages/folds.tsx index ae7981dc..4b4b8892 100644 --- a/web/pages/folds.tsx +++ b/web/pages/folds.tsx @@ -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)) diff --git a/web/pages/landing-page.tsx b/web/pages/landing-page.tsx index d45c0aef..334b5513 100644 --- a/web/pages/landing-page.tsx +++ b/web/pages/landing-page.tsx @@ -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"> diff --git a/web/pages/simulator.tsx b/web/pages/simulator.tsx index b253ac29..7c48302e 100644 --- a/web/pages/simulator.tsx +++ b/web/pages/simulator.tsx @@ -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>