Move tabs to sidebar

This commit is contained in:
Pico2x 2022-09-13 17:09:49 +01:00
parent 483838c1b2
commit e8ae9a7394
6 changed files with 362 additions and 102 deletions

View File

@ -0,0 +1,94 @@
import { BookOpenIcon, HomeIcon } from '@heroicons/react/outline'
import { Item } from './sidebar'
import clsx from 'clsx'
import { trackCallback } from 'web/lib/service/analytics'
import TrophyIcon from 'web/lib/icons/trophy-icon'
import { useUser } from 'web/hooks/use-user'
import NotificationsIcon from '../notifications-icon'
import router from 'next/router'
import { userProfileItem } from './nav-bar'
const mobileGroupNavigation = [
{ name: 'About', key: 'about', icon: BookOpenIcon },
{ name: 'Markets', key: 'markets', icon: HomeIcon },
{ name: 'Leaderboard', key: 'leaderboards', icon: TrophyIcon },
]
const mobileGeneralNavigation = [
{
name: 'Notifications',
key: 'notifications',
icon: NotificationsIcon,
href: '/notifications',
},
]
export function GroupNavBar(props: {
currentPage: string
onClick: (key: string) => void
}) {
const { currentPage } = props
const user = useUser()
return (
<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">
{mobileGroupNavigation.map((item) => (
<NavBarItem
key={item.name}
item={item}
currentPage={currentPage}
onClick={props.onClick}
/>
))}
{mobileGeneralNavigation.map((item) => (
<NavBarItem
key={item.name}
item={item}
currentPage={currentPage}
onClick={() => {
router.push(item.href)
}}
/>
))}
{user && (
<NavBarItem
key={'profile'}
currentPage={currentPage}
onClick={() => {
router.push(`/${user.username}?tab=trades`)
}}
item={userProfileItem(user)}
/>
)}
</nav>
)
}
function NavBarItem(props: {
item: Item
currentPage: string
onClick: (key: string) => void
}) {
const { item, currentPage } = props
const track = trackCallback(
`group navbar: ${item.trackingEventName ?? item.name}`
)
return (
<button onClick={() => props.onClick(item.key ?? '#')}>
<a
className={clsx(
'block w-full py-1 px-3 text-center hover:bg-indigo-200 hover:text-indigo-700',
currentPage === item.key && 'bg-gray-200 text-indigo-700'
)}
onClick={track}
>
{item.icon && <item.icon className="my-1 mx-auto h-6 w-6" />}
{item.name}
</a>
</button>
)
}

View File

@ -0,0 +1,87 @@
import { HomeIcon, BookOpenIcon } from '@heroicons/react/outline'
import clsx from 'clsx'
import { useUser } from 'web/hooks/use-user'
import { ManifoldLogo } from './manifold-logo'
import { ProfileSummary } from './profile-menu'
import React from 'react'
import TrophyIcon from 'web/lib/icons/trophy-icon'
import { SignInButton } from '../sign-in-button'
import CornerDownRightIcon from 'web/lib/icons/corner-down-right-icon'
import NotificationsIcon from '../notifications-icon'
import { SidebarItem } from './sidebar'
import { buildArray } from 'common/util/array'
import { User } from 'common/user'
const groupNavigation = [
{ name: 'About', key: 'about', icon: BookOpenIcon },
{ name: 'Markets', key: 'markets', icon: HomeIcon },
{ name: 'Leaderboard', key: 'leaderboards', icon: TrophyIcon },
]
const generalNavigation = (user?: User | null) =>
buildArray(
user && {
name: 'Notifications',
href: `/notifications`,
key: 'notifications',
icon: NotificationsIcon,
}
)
export default function GroupSidebar(props: {
groupName: string
className?: string
onClick: (key: string) => void
joinOrAddQuestionsButton: React.ReactNode
currentKey: string
}) {
const { className, groupName, currentKey } = props
const user = useUser()
return (
<nav
aria-label="Group Sidebar"
className={clsx('flex max-h-[100vh] flex-col', className)}
>
<ManifoldLogo className="pt-6" twoLine />
<div className=" min-h-0 shrink flex-col items-stretch gap-1 pt-6 lg:flex ">
{user ? (
<ProfileSummary user={user} />
) : (
<SignInButton className="mb-4" />
)}
</div>
<div className={' text-2xl text-indigo-700 sm:mb-1 sm:mt-3'}>
{groupName}
</div>
{/* Desktop navigation */}
<div className="relative hidden min-h-0 shrink flex-col items-stretch gap-1 pl-6 pb-0 lg:flex">
{/* adds a purple CornerDownRightIcon pointing to the sidebaritems */}
<CornerDownRightIcon className="absolute left-0 top-0 mt-2 h-6 w-6 text-indigo-700" />
{groupNavigation.map((item) => (
<SidebarItem
key={item.key}
item={item}
currentPage={currentKey}
onClick={props.onClick}
/>
))}
</div>
{generalNavigation(user).map((item) => (
<SidebarItem
key={item.key}
item={item}
currentPage={currentKey}
onClick={props.onClick}
/>
))}
{props.joinOrAddQuestionsButton}
</nav>
)
}

View File

@ -17,6 +17,7 @@ import { useRouter } from 'next/router'
import NotificationsIcon from 'web/components/notifications-icon' import NotificationsIcon from 'web/components/notifications-icon'
import { useIsIframe } from 'web/hooks/use-is-iframe' import { useIsIframe } from 'web/hooks/use-is-iframe'
import { trackCallback } from 'web/lib/service/analytics' import { trackCallback } from 'web/lib/service/analytics'
import { User } from 'common/user'
function getNavigation() { function getNavigation() {
return [ return [
@ -34,6 +35,21 @@ const signedOutNavigation = [
{ name: 'Explore', href: '/home', icon: SearchIcon }, { name: 'Explore', href: '/home', icon: SearchIcon },
] ]
export const userProfileItem = (user: User) => ({
name: formatMoney(user.balance),
trackingEventName: 'profile',
href: `/${user.username}?tab=trades`,
icon: () => (
<Avatar
className="mx-auto my-1"
size="xs"
username={user.username}
avatarUrl={user.avatarUrl}
noLink
/>
),
})
// From https://codepen.io/chris__sev/pen/QWGvYbL // From https://codepen.io/chris__sev/pen/QWGvYbL
export function BottomNavBar() { export function BottomNavBar() {
const [sidebarOpen, setSidebarOpen] = useState(false) const [sidebarOpen, setSidebarOpen] = useState(false)
@ -61,20 +77,7 @@ export function BottomNavBar() {
<NavBarItem <NavBarItem
key={'profile'} key={'profile'}
currentPage={currentPage} currentPage={currentPage}
item={{ item={userProfileItem(user)}
name: formatMoney(user.balance),
trackingEventName: 'profile',
href: `/${user.username}?tab=trades`,
icon: () => (
<Avatar
className="mx-auto my-1"
size="xs"
username={user.username}
avatarUrl={user.avatarUrl}
noLink
/>
),
}}
/> />
)} )}
<div <div
@ -98,7 +101,7 @@ function NavBarItem(props: { item: Item; currentPage: string }) {
const track = trackCallback(`navbar: ${item.trackingEventName ?? item.name}`) const track = trackCallback(`navbar: ${item.trackingEventName ?? item.name}`)
return ( return (
<Link href={item.href}> <Link href={item.href ?? '#'}>
<a <a
className={clsx( className={clsx(
'block w-full py-1 px-3 text-center hover:bg-indigo-200 hover:text-indigo-700', 'block w-full py-1 px-3 text-center hover:bg-indigo-200 hover:text-indigo-700',

View File

@ -13,7 +13,7 @@ import Router, { useRouter } from 'next/router'
import { useUser } from 'web/hooks/use-user' import { useUser } from 'web/hooks/use-user'
import { firebaseLogout, User } from 'web/lib/firebase/users' import { firebaseLogout, User } from 'web/lib/firebase/users'
import { ManifoldLogo } from './manifold-logo' import { ManifoldLogo } from './manifold-logo'
import { MenuButton } from './menu' import { MenuButton, MenuItem } from './menu'
import { ProfileSummary } from './profile-menu' import { ProfileSummary } from './profile-menu'
import NotificationsIcon from 'web/components/notifications-icon' import NotificationsIcon from 'web/components/notifications-icon'
import React from 'react' import React from 'react'
@ -139,7 +139,7 @@ function getMoreMobileNav() {
} }
if (IS_PRIVATE_MANIFOLD) return [signOut] if (IS_PRIVATE_MANIFOLD) return [signOut]
return buildArray<Item>( return buildArray<MenuItem>(
CHALLENGES_ENABLED && { name: 'Challenges', href: '/challenges' }, CHALLENGES_ENABLED && { name: 'Challenges', href: '/challenges' },
[ [
{ name: 'Groups', href: '/groups' }, { name: 'Groups', href: '/groups' },
@ -156,39 +156,59 @@ function getMoreMobileNav() {
export type Item = { export type Item = {
name: string name: string
trackingEventName?: string trackingEventName?: string
href: string href?: string
key?: string
icon?: React.ComponentType<{ className?: string }> icon?: React.ComponentType<{ className?: string }>
} }
function SidebarItem(props: { item: Item; currentPage: string }) { export function SidebarItem(props: {
const { item, currentPage } = props item: Item
return ( currentPage: string
<Link href={item.href} key={item.name}> onClick?: (key: string) => void
<a }) {
onClick={trackCallback('sidebar: ' + item.name)} const { item, currentPage, onClick } = props
className={clsx( const isCurrentPage =
item.href == currentPage item.href != null ? item.href === currentPage : item.key === currentPage
? 'bg-gray-200 text-gray-900'
: 'text-gray-600 hover:bg-gray-100', const sidebarItem = (
'group flex items-center rounded-md px-3 py-2 text-sm font-medium' <a
)} onClick={trackCallback('sidebar: ' + item.name)}
aria-current={item.href == currentPage ? 'page' : undefined} className={clsx(
> isCurrentPage
{item.icon && ( ? 'bg-gray-200 text-gray-900'
<item.icon : 'text-gray-600 hover:bg-gray-100',
className={clsx( 'group flex items-center rounded-md px-3 py-2 text-sm font-medium'
item.href == currentPage )}
? 'text-gray-500' aria-current={item.href == currentPage ? 'page' : undefined}
: 'text-gray-400 group-hover:text-gray-500', >
'-ml-1 mr-3 h-6 w-6 flex-shrink-0' {item.icon && (
)} <item.icon
aria-hidden="true" className={clsx(
/> isCurrentPage
)} ? 'text-gray-500'
<span className="truncate">{item.name}</span> : 'text-gray-400 group-hover:text-gray-500',
</a> '-ml-1 mr-3 h-6 w-6 flex-shrink-0'
</Link> )}
aria-hidden="true"
/>
)}
<span className="truncate">{item.name}</span>
</a>
) )
if (item.href) {
return (
<Link href={item.href} key={item.name}>
{sidebarItem}
</Link>
)
} else {
return onClick ? (
<button onClick={() => onClick(item.key ?? '#')}>{sidebarItem}</button>
) : (
<> </>
)
}
} }
function SidebarButton(props: { function SidebarButton(props: {

View File

@ -0,0 +1,19 @@
export default function CornerDownRightIcon(props: { className?: string }) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
className={props.className}
>
<polyline points="15 10 20 15 15 20"></polyline>
<path d="M4 4v7a4 4 0 0 0 4 4h12"></path>
</svg>
)
}

View File

@ -1,10 +1,9 @@
import React, { useState } from 'react' import React, { useState } from 'react'
import Link from 'next/link' import Link from 'next/link'
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
import { toast } from 'react-hot-toast' import { toast, Toaster } from 'react-hot-toast'
import { Group, GROUP_CHAT_SLUG } from 'common/group' import { Group, GROUP_CHAT_SLUG } from 'common/group'
import { Page } from 'web/components/page'
import { Contract, listContractsByGroupSlug } from 'web/lib/firebase/contracts' import { Contract, listContractsByGroupSlug } from 'web/lib/firebase/contracts'
import { import {
addContractToGroup, addContractToGroup,
@ -30,7 +29,6 @@ import Custom404 from '../../404'
import { SEO } from 'web/components/SEO' import { SEO } from 'web/components/SEO'
import { Linkify } from 'web/components/linkify' import { Linkify } from 'web/components/linkify'
import { fromPropz, usePropz } from 'web/hooks/use-propz' import { fromPropz, usePropz } from 'web/hooks/use-propz'
import { Tabs } from 'web/components/layout/tabs'
import { LoadingIndicator } from 'web/components/loading-indicator' import { LoadingIndicator } from 'web/components/loading-indicator'
import { Modal } from 'web/components/layout/modal' import { Modal } from 'web/components/layout/modal'
import { ChoicesToggleGroup } from 'web/components/choices-toggle-group' import { ChoicesToggleGroup } from 'web/components/choices-toggle-group'
@ -51,6 +49,9 @@ import { Spacer } from 'web/components/layout/spacer'
import { usePost } from 'web/hooks/use-post' import { usePost } from 'web/hooks/use-post'
import { useAdmin } from 'web/hooks/use-admin' import { useAdmin } from 'web/hooks/use-admin'
import { track } from '@amplitude/analytics-browser' import { track } from '@amplitude/analytics-browser'
import GroupSidebar from 'web/components/nav/group-sidebar'
import { GroupNavBar } from 'web/components/nav/group-nav-bar'
import { ArrowLeftIcon } from '@heroicons/react/solid'
export const getStaticProps = fromPropz(getStaticPropz) export const getStaticProps = fromPropz(getStaticPropz)
export async function getStaticPropz(props: { params: { slugs: string[] } }) { export async function getStaticPropz(props: { params: { slugs: string[] } }) {
@ -144,6 +145,7 @@ export default function GroupPage(props: {
const user = useUser() const user = useUser()
const isAdmin = useAdmin() const isAdmin = useAdmin()
const memberIds = useMemberIds(group?.id ?? null) ?? props.memberIds const memberIds = useMemberIds(group?.id ?? null) ?? props.memberIds
const [sidebarIndex, setSidebarIndex] = useState(0)
useSaveReferral(user, { useSaveReferral(user, {
defaultReferrerUsername: creator.username, defaultReferrerUsername: creator.username,
@ -157,7 +159,7 @@ export default function GroupPage(props: {
const isMember = user && memberIds.includes(user.id) const isMember = user && memberIds.includes(user.id)
const maxLeaderboardSize = 50 const maxLeaderboardSize = 50
const leaderboard = ( const leaderboardPage = (
<Col> <Col>
<div className="mt-4 flex flex-col gap-8 px-4 md:flex-row"> <div className="mt-4 flex flex-col gap-8 px-4 md:flex-row">
<GroupLeaderboard <GroupLeaderboard
@ -176,7 +178,7 @@ export default function GroupPage(props: {
</Col> </Col>
) )
const aboutTab = ( const aboutPage = (
<Col> <Col>
{(group.aboutPostId != null || isCreator || isAdmin) && ( {(group.aboutPostId != null || isCreator || isAdmin) && (
<GroupAboutPost <GroupAboutPost
@ -196,74 +198,106 @@ export default function GroupPage(props: {
</Col> </Col>
) )
const questionsTab = ( const questionsPage = (
<ContractSearch <>
user={user} {/* align the divs to the right */}
defaultSort={'newest'} <div className={' flex justify-end px-2 pb-2 sm:hidden'}>
defaultFilter={suggestedFilter} <div>
additionalFilter={{ groupSlug: group.slug }} <JoinGroupButton group={group} user={user} />
persistPrefix={`group-${group.slug}`} </div>
/> </div>
<ContractSearch
user={user}
defaultSort={'newest'}
defaultFilter={suggestedFilter}
additionalFilter={{ groupSlug: group.slug }}
persistPrefix={`group-${group.slug}`}
/>
</>
) )
const tabs = [ const sidebarPages = [
{ {
badge: `${contractsCount}`, badge: `${contractsCount}`,
title: 'Markets', title: 'Markets',
content: questionsTab, content: questionsPage,
href: groupPath(group.slug, 'markets'), href: groupPath(group.slug, 'markets'),
key: 'markets',
}, },
{ {
title: 'Leaderboards', title: 'Leaderboards',
content: leaderboard, content: leaderboardPage,
href: groupPath(group.slug, 'leaderboards'), href: groupPath(group.slug, 'leaderboards'),
key: 'leaderboards',
}, },
{ {
title: 'About', title: 'About',
content: aboutTab, content: aboutPage,
href: groupPath(group.slug, 'about'), href: groupPath(group.slug, 'about'),
key: 'about',
}, },
] ]
const tabIndex = tabs const pageContent = sidebarPages[sidebarIndex].content
.map((t) => t.title.toLowerCase()) const onSidebarClick = (key: string) => {
.indexOf(page ?? 'markets') const index = sidebarPages.findIndex((t) => t.key === key)
setSidebarIndex(index)
}
const joinOrAddQuestionsButton = (
<JoinOrAddQuestionsButtons
group={group}
user={user}
isMember={!!isMember}
/>
)
return ( return (
<Page> <>
<SEO <header className="sticky top-0 z-50 w-full pb-2 md:hidden lg:col-span-12">
title={group.name} <div className="flex items-center border-b border-gray-200 bg-white px-4">
description={`Created by ${creator.name}. ${group.about}`} <div className="flex-shrink-0">
url={groupPath(group.slug)} <Link href="/">
/> <a className="text-indigo-700 hover:text-gray-500 ">
<Col className="relative px-3"> <ArrowLeftIcon className="h-5 w-5" aria-hidden="true" />
<Row className={'items-center justify-between gap-4'}> </a>
<div className={'sm:mb-1'}> </Link>
<div </div>
className={'line-clamp-1 my-2 text-2xl text-indigo-700 sm:my-3'} <div className="ml-3">
> <h1 className="text-lg font-medium text-indigo-700">
{group.name} {group.name}
</div> </h1>
<div className={'hidden sm:block'}>
<Linkify text={group.about} />
</div>
</div> </div>
<div className="mt-2"> </div>
<JoinOrAddQuestionsButtons </header>
group={group} <div>
user={user} <div
isMember={!!isMember} className={
/> 'mx-auto w-full pb-[58px] lg:grid lg:grid-cols-12 lg:gap-x-2 lg:pb-0 xl:max-w-7xl xl:gap-x-8'
</div> }
</Row> >
</Col> <Toaster />
<Tabs <GroupSidebar
currentPageForAnalytics={groupPath(group.slug)} groupName={group.name}
className={'mx-2 mb-0 sm:mb-2'} className="sticky top-0 hidden divide-gray-300 self-start pl-2 lg:col-span-2 lg:flex"
defaultIndex={tabIndex > 0 ? tabIndex : 0} onClick={onSidebarClick}
tabs={tabs} joinOrAddQuestionsButton={joinOrAddQuestionsButton}
/> currentKey={sidebarPages[sidebarIndex].key}
</Page> />
<SEO
title={group.name}
description={`Created by ${creator.name}. ${group.about}`}
url={groupPath(group.slug)}
/>
<main className={'px-2 pt-1 xl:col-span-8'}>{pageContent}</main>
</div>
<GroupNavBar
currentPage={sidebarPages[sidebarIndex].key}
onClick={onSidebarClick}
/>
</div>
</>
) )
} }
@ -271,10 +305,11 @@ function JoinOrAddQuestionsButtons(props: {
group: Group group: Group
user: User | null | undefined user: User | null | undefined
isMember: boolean isMember: boolean
className?: string
}) { }) {
const { group, user, isMember } = props const { group, user, isMember } = props
return user && isMember ? ( return user && isMember ? (
<Row className={'mt-0 justify-end'}> <Row className={'w-full self-start pt-4'}>
<AddContractButton group={group} user={user} /> <AddContractButton group={group} user={user} />
</Row> </Row>
) : group.anyoneCanJoin ? ( ) : group.anyoneCanJoin ? (
@ -433,9 +468,9 @@ function AddContractButton(props: { group: Group; user: User }) {
return ( return (
<> <>
<div className={'flex justify-center'}> <div className={'flex w-full justify-center'}>
<Button <Button
className="whitespace-nowrap" className="w-full whitespace-nowrap"
size="md" size="md"
color="indigo" color="indigo"
onClick={() => setOpen(true)} onClick={() => setOpen(true)}
@ -534,7 +569,9 @@ function JoinGroupButton(props: {
<div> <div>
<button <button
onClick={follow} onClick={follow}
className={'btn-md btn-outline btn whitespace-nowrap normal-case'} className={
'btn-md btn-outline btn w-full whitespace-nowrap normal-case'
}
> >
Follow Follow
</button> </button>