import clsx from 'clsx' import {ReadonlyURLSearchParams, usePathname} from 'next/navigation' import {useRouter} from 'next/router' import {ReactNode, useEffect, useRef} from 'react' import {Row} from 'web/components/layout/row' import {Carousel} from 'web/components/widgets/carousel' import {Tooltip} from 'web/components/widgets/tooltip' import {useDefinedSearchParams} from 'web/hooks/use-defined-search-params' import {usePersistentInMemoryState} from 'web/hooks/use-persistent-in-memory-state' import {usePersistentLocalState} from 'web/hooks/use-persistent-local-state' import {track} from 'web/lib/service/analytics' import {Col} from './col' export type Tab = { title: string content: ReactNode stackedTabIcon?: ReactNode inlineTabIcon?: ReactNode tooltip?: string prerender?: boolean className?: string queryString?: string } type TabProps = { tabs: Tab[] labelClassName?: string onClick?: (tabTitleOrQueryTitle: string, index: number) => void className?: string labelsParentClassName?: string trackingName?: string // Default is to lazy render tabs as they are selected. If true, it will render all tabs at once. renderAllTabs?: boolean name?: string // a unique identifier for the tabs, used for caching } export function MinimalistTabs(props: TabProps & {activeIndex: number}) { const {tabs, activeIndex, labelClassName, onClick, className, renderAllTabs, trackingName} = props const hasRenderedIndexRef = useRef(new Set()) hasRenderedIndexRef.current.add(activeIndex) return ( <> {tabs.map((tab, i) => ( ) => { e.preventDefault() onClick?.(tab.queryString?.toLowerCase() ?? tab.title.toLowerCase(), i) if (trackingName) { track(trackingName, { tab: tab.title, }) } }} aria-current={activeIndex === i ? 'page' : undefined} className={clsx( activeIndex === i ? 'text-primary-600' : 'text-ink-400 hover:text-ink-700', 'cursor-pointer whitespace-nowrap text-lg ', labelClassName, )} > {tab.title} ))} {tabs .map((tab, i) => ({tab, i})) .filter(({tab, i}) => renderAllTabs || tab.prerender || hasRenderedIndexRef.current.has(i)) .map(({tab, i}) => (
{tab.content}
))} ) } export function ControlledTabs(props: TabProps & {activeIndex: number}) { const { tabs, activeIndex, labelClassName, onClick, className, renderAllTabs, labelsParentClassName, trackingName, } = props const hasRenderedIndexRef = useRef(new Set()) hasRenderedIndexRef.current.add(activeIndex) return ( <> {tabs.map((tab, i) => ( ) => { e.preventDefault() onClick?.(tab.queryString?.toLowerCase() ?? tab.title.toLowerCase(), i) if (trackingName) { track(trackingName, { tab: tab.title, }) } }} className={clsx( activeIndex === i ? 'border-primary-500 text-primary-600' : 'text-ink-500 hover:border-ink-300 hover:text-ink-700 border-transparent', 'mr-4 inline-flex cursor-pointer flex-row gap-1 whitespace-nowrap border-b-2 px-1 py-3 text-sm font-medium ', labelClassName, 'flex-shrink-0', )} aria-current={activeIndex === i ? 'page' : undefined} > {tab.stackedTabIcon && {tab.stackedTabIcon}} {tab.title.split('\n').map((line, i) => ( {line} ))} {tab.inlineTabIcon} ))} {tabs .map((tab, i) => ({tab, i})) .filter(({tab, i}) => renderAllTabs || tab.prerender || hasRenderedIndexRef.current.has(i)) .map(({tab, i}) => (
{tab.content}
))} ) } export function UncontrolledTabs(props: TabProps & {defaultIndex?: number}) { const {defaultIndex, onClick, ...rest} = props const [activeIndex, setActiveIndex] = usePersistentInMemoryState( defaultIndex ?? 0, `tab-${props.trackingName}-${props.name ?? props.tabs[0]?.title}`, ) if ((defaultIndex ?? 0) > props.tabs.length - 1) { console.error('default index greater than tabs length') } return ( { setActiveIndex(i) onClick?.(titleOrQueryTitle, i) }} labelsParentClassName={'gap-0 xs:gap-4'} /> ) } const isTabSelected = (params: ReadonlyURLSearchParams, queryParam: string, tab: Tab) => { const selected = params.get(queryParam) if (typeof selected === 'string') { return (tab.queryString?.toLowerCase() ?? tab.title.toLowerCase()) === selected } else { return false } } export function QueryUncontrolledTabs( props: TabProps & { defaultIndex?: number scrollToTop?: boolean minimalist?: boolean saveTabInLocalStorageKey?: string }, ) { const {tabs, minimalist, onClick, scrollToTop, saveTabInLocalStorageKey, ...rest} = props const router = useRouter() const pathName = usePathname() const {searchParams, createQueryString} = useDefinedSearchParams() const selectedIdx = tabs.findIndex((t) => isTabSelected(searchParams, 'tab', t)) const [savedTabIndex, setSavedTabIndex] = usePersistentLocalState( undefined, saveTabInLocalStorageKey ?? '', ) const defaultIndex = (saveTabInLocalStorageKey ? savedTabIndex : undefined) ?? props.defaultIndex ?? 0 const activeIndex = selectedIdx !== -1 ? selectedIdx : defaultIndex useEffect(() => { if (onClick) { onClick(tabs[activeIndex].queryString ?? tabs[activeIndex].title, activeIndex) } if (saveTabInLocalStorageKey) setSavedTabIndex(activeIndex) }, [activeIndex]) if (minimalist) { return ( { if (scrollToTop) window.scrollTo({top: 0}) router.replace(pathName + '?' + createQueryString('tab', title), undefined, { shallow: true, }) }} /> ) } return ( { if (scrollToTop) window.scrollTo({top: 0}) router.replace(pathName + '?' + createQueryString('tab', title), undefined, {shallow: true}) }} /> ) }