Make tabs components better (#691)

* Make better tabs components, apply to user page

* Remove fishy unused href property from tabs

* Remove tab ID property

* Clean up crufty markup in tabs component

* Fix naming to be right (thanks James!)
This commit is contained in:
Marshall Polaris 2022-07-25 13:27:09 -07:00 committed by GitHub
parent e4f8c14fab
commit 64462d6ab4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 102 additions and 82 deletions

View File

@ -1,82 +1,121 @@
import clsx from 'clsx' import clsx from 'clsx'
import Link from 'next/link' import { useRouter, NextRouter } from 'next/router'
import { ReactNode, useState } from 'react' import { ReactNode, useState } from 'react'
import { Row } from './row'
import { track } from '@amplitude/analytics-browser' import { track } from '@amplitude/analytics-browser'
type Tab = { type Tab = {
title: string title: string
tabIcon?: ReactNode tabIcon?: ReactNode
content: ReactNode content: ReactNode
// If set, change the url to this href when the tab is selected
href?: string
// If set, show a badge with this content // If set, show a badge with this content
badge?: string badge?: string
} }
export function Tabs(props: { type TabProps = {
tabs: Tab[] tabs: Tab[]
defaultIndex?: number
labelClassName?: string labelClassName?: string
onClick?: (tabTitle: string, index: number) => void onClick?: (tabTitle: string, index: number) => void
className?: string className?: string
currentPageForAnalytics?: string currentPageForAnalytics?: string
}) { }
export function ControlledTabs(props: TabProps & { activeIndex: number }) {
const { const {
tabs, tabs,
defaultIndex, activeIndex,
labelClassName, labelClassName,
onClick, onClick,
className, className,
currentPageForAnalytics, currentPageForAnalytics,
} = props } = props
const [activeIndex, setActiveIndex] = useState(defaultIndex ?? 0)
const activeTab = tabs[activeIndex] as Tab | undefined // can be undefined in weird case const activeTab = tabs[activeIndex] as Tab | undefined // can be undefined in weird case
return ( return (
<> <>
<div className={clsx('mb-4 border-b border-gray-200', className)}> <nav
<nav className="-mb-px flex space-x-8" aria-label="Tabs"> className={clsx('mb-4 space-x-8 border-b border-gray-200', className)}
{tabs.map((tab, i) => ( aria-label="Tabs"
<Link href={tab.href ?? '#'} key={tab.title} shallow={!!tab.href}> >
<a {tabs.map((tab, i) => (
id={`tab-${i}`} <a
key={tab.title} href="#"
onClick={(e) => { key={tab.title}
track('Clicked Tab', { onClick={(e) => {
title: tab.title, e.preventDefault()
href: tab.href, track('Clicked Tab', {
currentPage: currentPageForAnalytics, title: tab.title,
}) currentPage: currentPageForAnalytics,
if (!tab.href) { })
e.preventDefault() onClick?.(tab.title, i)
} }}
setActiveIndex(i) className={clsx(
onClick?.(tab.title, i) activeIndex === i
}} ? 'border-indigo-500 text-indigo-600'
className={clsx( : 'border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700',
activeIndex === i 'inline-flex cursor-pointer flex-row gap-1 whitespace-nowrap border-b-2 px-1 py-3 text-sm font-medium',
? 'border-indigo-500 text-indigo-600' labelClassName
: 'border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700', )}
'cursor-pointer whitespace-nowrap border-b-2 py-3 px-1 text-sm font-medium', aria-current={activeIndex === i ? 'page' : undefined}
labelClassName >
)} {tab.tabIcon && <span>{tab.tabIcon}</span>}
aria-current={activeIndex === i ? 'page' : undefined} {tab.badge ? (
> <span className="px-0.5 font-bold">{tab.badge}</span>
<Row className={'items-center justify-center gap-1'}> ) : null}
{tab.tabIcon && <span> {tab.tabIcon}</span>} {tab.title}
{tab.badge ? ( </a>
<div className="px-0.5 font-bold">{tab.badge}</div> ))}
) : null} </nav>
{tab.title}
</Row>
</a>
</Link>
))}
</nav>
</div>
{activeTab?.content} {activeTab?.content}
</> </>
) )
} }
export function UncontrolledTabs(props: TabProps & { defaultIndex?: number }) {
const { defaultIndex, onClick, ...rest } = props
const [activeIndex, setActiveIndex] = useState(defaultIndex ?? 0)
return (
<ControlledTabs
{...rest}
activeIndex={activeIndex}
onClick={(title, i) => {
setActiveIndex(i)
onClick?.(title, i)
}}
/>
)
}
const isTabSelected = (router: NextRouter, queryParam: string, tab: Tab) => {
const selected = router.query[queryParam]
if (typeof selected === 'string') {
return tab.title.toLowerCase() === selected
} else {
return false
}
}
export function QueryUncontrolledTabs(
props: TabProps & { defaultIndex?: number }
) {
const { tabs, defaultIndex, onClick, ...rest } = props
const router = useRouter()
const selectedIdx = tabs.findIndex((t) => isTabSelected(router, 'tab', t))
const activeIndex = selectedIdx !== -1 ? selectedIdx : defaultIndex ?? 0
return (
<ControlledTabs
{...rest}
tabs={tabs}
activeIndex={activeIndex}
onClick={(title, i) => {
router.replace(
{ query: { ...router.query, tab: title.toLowerCase() } },
undefined,
{ shallow: true }
)
onClick?.(title, i)
}}
/>
)
}
// legacy code that didn't know about any other kind of tabs imports this
export const Tabs = UncontrolledTabs

View File

@ -22,7 +22,7 @@ import { Linkify } from './linkify'
import { Spacer } from './layout/spacer' import { Spacer } from './layout/spacer'
import { Row } from './layout/row' import { Row } from './layout/row'
import { genHash } from 'common/util/random' import { genHash } from 'common/util/random'
import { Tabs } from './layout/tabs' import { QueryUncontrolledTabs } from './layout/tabs'
import { UserCommentsList } from './comments-list' import { UserCommentsList } from './comments-list'
import { useWindowSize } from 'web/hooks/use-window-size' import { useWindowSize } from 'web/hooks/use-window-size'
import { Comment, getUsersComments } from 'web/lib/firebase/comments' import { Comment, getUsersComments } from 'web/lib/firebase/comments'
@ -64,12 +64,8 @@ export function UserLink(props: {
export const TAB_IDS = ['markets', 'comments', 'bets', 'groups'] export const TAB_IDS = ['markets', 'comments', 'bets', 'groups']
const JUNE_1_2022 = new Date('2022-06-01T00:00:00.000Z').valueOf() const JUNE_1_2022 = new Date('2022-06-01T00:00:00.000Z').valueOf()
export function UserPage(props: { export function UserPage(props: { user: User; currentUser?: User }) {
user: User const { user, currentUser } = props
currentUser?: User
defaultTabTitle?: string | undefined
}) {
const { user, currentUser, defaultTabTitle } = props
const router = useRouter() const router = useRouter()
const isCurrentUser = user.id === currentUser?.id const isCurrentUser = user.id === currentUser?.id
const bannerUrl = user.bannerUrl ?? defaultBannerUrl(user.id) const bannerUrl = user.bannerUrl ?? defaultBannerUrl(user.id)
@ -276,29 +272,17 @@ export function UserPage(props: {
<Spacer h={10} /> <Spacer h={10} />
{usersContracts !== 'loading' && contractsById && usersComments ? ( {usersContracts !== 'loading' && contractsById && usersComments ? (
<Tabs <QueryUncontrolledTabs
currentPageForAnalytics={'profile'} currentPageForAnalytics={'profile'}
labelClassName={'pb-2 pt-1 '} labelClassName={'pb-2 pt-1 '}
defaultIndex={
defaultTabTitle ? TAB_IDS.indexOf(defaultTabTitle) : 0
}
onClick={(tabName) => {
const tabId = tabName.toLowerCase()
const subpath = tabId === 'markets' ? '' : '?tab=' + tabId
// BUG: if you start on `/Bob/bets`, then click on Markets, use-query-and-sort-params
// rewrites the url incorrectly to `/Bob/bets` instead of `/Bob`
router.push(`/${user.username}${subpath}`, undefined, {
shallow: true,
})
}}
tabs={[ tabs={[
{ {
title: 'Markets', title: 'Markets',
content: <CreatorContractsList creator={user} />, content: <CreatorContractsList creator={user} />,
tabIcon: ( tabIcon: (
<div className="px-0.5 font-bold"> <span className="px-0.5 font-bold">
{usersContracts.length} {usersContracts.length}
</div> </span>
), ),
}, },
{ {
@ -311,7 +295,9 @@ export function UserPage(props: {
/> />
), ),
tabIcon: ( tabIcon: (
<div className="px-0.5 font-bold">{usersComments.length}</div> <span className="px-0.5 font-bold">
{usersComments.length}
</span>
), ),
}, },
{ {
@ -329,7 +315,7 @@ export function UserPage(props: {
/> />
</div> </div>
), ),
tabIcon: <div className="px-0.5 font-bold">{betCount}</div>, tabIcon: <span className="px-0.5 font-bold">{betCount}</span>,
}, },
]} ]}
/> />

View File

@ -31,9 +31,8 @@ export default function UserProfile(props: { user: User | null }) {
const { user } = props const { user } = props
const router = useRouter() const router = useRouter()
const { username, tab } = router.query as { const { username } = router.query as {
username: string username: string
tab?: string | undefined
} }
const currentUser = useUser() const currentUser = useUser()
@ -42,11 +41,7 @@ export default function UserProfile(props: { user: User | null }) {
if (user === undefined) return <div /> if (user === undefined) return <div />
return user ? ( return user ? (
<UserPage <UserPage user={user} currentUser={currentUser || undefined} />
user={user}
currentUser={currentUser || undefined}
defaultTabTitle={tab}
/>
) : ( ) : (
<Custom404 /> <Custom404 />
) )