Implement double carousel for /experimental/home

This commit is contained in:
James Grugett 2022-08-30 16:18:39 -05:00
parent d658a48b66
commit f83b62cf50
4 changed files with 163 additions and 102 deletions

View File

@ -0,0 +1,60 @@
import { ChevronLeftIcon, ChevronRightIcon } from '@heroicons/react/solid'
import clsx from 'clsx'
import { throttle } from 'lodash'
import { ReactNode, useRef, useState, useEffect } from 'react'
import { Row } from './layout/row'
export function Carousel(props: { children: ReactNode; className?: string }) {
const { children, className } = props
const ref = useRef<HTMLDivElement>(null)
const th = (f: () => any) => throttle(f, 500, { trailing: false })
const scrollLeft = th(() =>
ref.current?.scrollBy({ left: -ref.current.clientWidth })
)
const scrollRight = th(() =>
ref.current?.scrollBy({ left: ref.current.clientWidth })
)
const [atFront, setAtFront] = useState(true)
const [atBack, setAtBack] = useState(false)
const onScroll = throttle(() => {
if (ref.current) {
const { scrollLeft, clientWidth, scrollWidth } = ref.current
setAtFront(scrollLeft < 80)
setAtBack(scrollWidth - (clientWidth + scrollLeft) < 80)
}
}, 500)
// eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(onScroll, [])
return (
<div className={clsx('relative', className)}>
<Row
className="scrollbar-hide w-full gap-4 overflow-x-auto scroll-smooth"
ref={ref}
onScroll={onScroll}
>
{children}
</Row>
{!atFront && (
<div
className="absolute left-0 top-0 bottom-0 z-10 flex w-10 cursor-pointer items-center justify-center hover:bg-indigo-100/30"
onMouseDown={scrollLeft}
>
<ChevronLeftIcon className="h-7 w-7 rounded-full bg-indigo-50 text-indigo-700" />
</div>
)}
{!atBack && (
<div
className="absolute right-0 top-0 bottom-0 z-10 flex w-10 cursor-pointer items-center justify-center hover:bg-indigo-100/30"
onMouseDown={scrollRight}
>
<ChevronRightIcon className="h-7 w-7 rounded-full bg-indigo-50 text-indigo-700" />
</div>
)}
</div>
)
}

View File

@ -10,7 +10,7 @@ import {
} from './contract/contracts-grid' } from './contract/contracts-grid'
import { ShowTime } from './contract/contract-details' import { ShowTime } from './contract/contract-details'
import { Row } from './layout/row' import { Row } from './layout/row'
import { useEffect, useLayoutEffect, useRef, useMemo } from 'react' import { useEffect, useLayoutEffect, useRef, useMemo, ReactNode } from 'react'
import { ENV, IS_PRIVATE_MANIFOLD } from 'common/envs/constants' import { ENV, IS_PRIVATE_MANIFOLD } from 'common/envs/constants'
import { useFollows } from 'web/hooks/use-follows' import { useFollows } from 'web/hooks/use-follows'
import { import {
@ -85,6 +85,7 @@ export function ContractSearch(props: {
isWholePage?: boolean isWholePage?: boolean
maxItems?: number maxItems?: number
noControls?: boolean noControls?: boolean
renderContracts?: (contracts: Contract[] | undefined) => ReactNode
}) { }) {
const { const {
user, user,
@ -101,6 +102,7 @@ export function ContractSearch(props: {
isWholePage, isWholePage,
maxItems, maxItems,
noControls, noControls,
renderContracts,
} = props } = props
const [state, setState] = usePersistentState( const [state, setState] = usePersistentState(
@ -203,14 +205,18 @@ export function ContractSearch(props: {
onSearchParametersChanged={onSearchParametersChanged} onSearchParametersChanged={onSearchParametersChanged}
noControls={noControls} noControls={noControls}
/> />
<ContractsGrid {renderContracts ? (
contracts={renderedContracts} renderContracts(renderedContracts)
loadMore={noControls ? undefined : performQuery} ) : (
showTime={state.showTime ?? undefined} <ContractsGrid
onContractClick={onContractClick} contracts={renderedContracts}
highlightOptions={highlightOptions} loadMore={noControls ? undefined : performQuery}
cardHideOptions={cardHideOptions} showTime={state.showTime ?? undefined}
/> onContractClick={onContractClick}
highlightOptions={highlightOptions}
cardHideOptions={cardHideOptions}
/>
)}
</Col> </Col>
) )
} }

View File

@ -1,5 +1,5 @@
import React from 'react' import React from 'react'
import { useRouter } from 'next/router' import Router from 'next/router'
import { PlusSmIcon } from '@heroicons/react/solid' import { PlusSmIcon } from '@heroicons/react/solid'
import { Page } from 'web/components/page' import { Page } from 'web/components/page'
@ -17,7 +17,13 @@ import { Button } from 'web/components/button'
import { Spacer } from 'web/components/layout/spacer' import { Spacer } from 'web/components/layout/spacer'
import { useMemberGroups } from 'web/hooks/use-group' import { useMemberGroups } from 'web/hooks/use-group'
import { Group } from 'common/group' import { Group } from 'common/group'
import { Title } from 'web/components/title' import { Carousel } from 'web/components/carousel'
import { LoadingIndicator } from 'web/components/loading-indicator'
import { ContractCard } from 'web/components/contract/contract-card'
import { range } from 'lodash'
import { Subtitle } from 'web/components/subtitle'
import { Contract } from 'common/contract'
import { ShowTime } from 'web/components/contract/contract-details'
export const getServerSideProps: GetServerSideProps = async (ctx) => { export const getServerSideProps: GetServerSideProps = async (ctx) => {
const creds = await authenticateOnServer(ctx) const creds = await authenticateOnServer(ctx)
@ -28,7 +34,6 @@ export const getServerSideProps: GetServerSideProps = async (ctx) => {
const Home = (props: { auth: { user: User } | null }) => { const Home = (props: { auth: { user: User } | null }) => {
const user = props.auth ? props.auth.user : null const user = props.auth ? props.auth.user : null
const router = useRouter()
useTracking('view home') useTracking('view home')
useSaveReferral() useSaveReferral()
@ -39,7 +44,7 @@ const Home = (props: { auth: { user: User } | null }) => {
return ( return (
<Page> <Page>
<Col className="mx-auto mb-8 w-full"> <Col className="mx-4 mt-4 gap-2 sm:mx-10 xl:w-[125%]">
<SearchSection label="Trending" sort="score" user={user} /> <SearchSection label="Trending" sort="score" user={user} />
<SearchSection label="Newest" sort="newest" user={user} /> <SearchSection label="Newest" sort="newest" user={user} />
<SearchSection label="Closing soon" sort="close-date" user={user} /> <SearchSection label="Closing soon" sort="close-date" user={user} />
@ -51,7 +56,7 @@ const Home = (props: { auth: { user: User } | null }) => {
type="button" type="button"
className="fixed bottom-[70px] right-3 inline-flex items-center rounded-full border border-transparent bg-indigo-600 p-3 text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 lg:hidden" className="fixed bottom-[70px] right-3 inline-flex items-center rounded-full border border-transparent bg-indigo-600 p-3 text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 lg:hidden"
onClick={() => { onClick={() => {
router.push('/create') Router.push('/create')
track('mobile create button') track('mobile create button')
}} }}
> >
@ -68,21 +73,25 @@ function SearchSection(props: {
}) { }) {
const { label, user, sort } = props const { label, user, sort } = props
const router = useRouter()
return ( return (
<Col> <Col>
<Title className="mx-2 !text-gray-800 sm:mx-0" text={label} /> <Subtitle className="mx-2 !mt-2 !text-gray-800 sm:mx-0" text={label} />
<Spacer h={2} /> <ContractSearch
<ContractSearch user={user} defaultSort={sort} maxItems={4} noControls /> user={user}
<Button defaultSort={sort}
className="self-end" maxItems={12}
color="blue" noControls
size="sm" renderContracts={(contracts) =>
onClick={() => router.push(`/home?s=${sort}`)} contracts ? (
> <DoubleCarousel
See more contracts={contracts}
</Button> seeMoreUrl={`/home?s=${sort}`}
/>
) : (
<LoadingIndicator />
)
}
/>
</Col> </Col>
) )
} }
@ -90,29 +99,74 @@ function SearchSection(props: {
function GroupSection(props: { group: Group; user: User | null }) { function GroupSection(props: { group: Group; user: User | null }) {
const { group, user } = props const { group, user } = props
const router = useRouter()
return ( return (
<Col className=""> <Col className="">
<Title className="mx-2 !text-gray-800 sm:mx-0" text={group.name} /> <Subtitle className="mx-2 !text-gray-800 sm:mx-0" text={group.name} />
<Spacer h={2} /> <Spacer h={2} />
<ContractSearch <ContractSearch
user={user} user={user}
defaultSort={'score'} defaultSort={'score'}
additionalFilter={{ groupSlug: group.slug }} additionalFilter={{ groupSlug: group.slug }}
maxItems={4} maxItems={12}
noControls noControls
renderContracts={(contracts) =>
contracts ? (
<DoubleCarousel
contracts={contracts}
seeMoreUrl={`/group/${group.slug}`}
/>
) : (
<LoadingIndicator />
)
}
/> />
<Button
className="mr-2 self-end"
color="blue"
size="sm"
onClick={() => router.push(`/group/${group.slug}`)}
>
See more
</Button>
</Col> </Col>
) )
} }
function DoubleCarousel(props: {
contracts: Contract[]
seeMoreUrl?: string
showTime?: ShowTime
}) {
const { contracts, seeMoreUrl, showTime } = props
return (
<Carousel className="-mx-4 mt-2 sm:-mx-10">
<div className="shrink-0 sm:w-6" />
{contracts &&
range(0, Math.floor(contracts.length / 2)).map((col) => {
const i = col * 2
return (
<Col>
<ContractCard
key={contracts[i].id}
contract={contracts[i]}
className="mb-2 max-h-[200px] w-96 shrink-0"
questionClass="line-clamp-3"
trackingPostfix=" tournament"
showTime={showTime}
/>
<ContractCard
key={contracts[i + 1].id}
contract={contracts[i + 1]}
className="mb-2 max-h-[200px] w-96 shrink-0"
questionClass="line-clamp-3"
trackingPostfix=" tournament"
showTime={showTime}
/>
</Col>
)
})}
<Button
className="self-center whitespace-nowrap"
color="blue"
size="sm"
onClick={() => seeMoreUrl && Router.push(seeMoreUrl)}
>
See more
</Button>
</Carousel>
)
}
export default Home export default Home

View File

@ -1,10 +1,5 @@
import { ClockIcon } from '@heroicons/react/outline' import { ClockIcon } from '@heroicons/react/outline'
import { import { UsersIcon } from '@heroicons/react/solid'
ChevronLeftIcon,
ChevronRightIcon,
UsersIcon,
} from '@heroicons/react/solid'
import clsx from 'clsx'
import { import {
BinaryContract, BinaryContract,
Contract, Contract,
@ -15,10 +10,10 @@ import dayjs, { Dayjs } from 'dayjs'
import customParseFormat from 'dayjs/plugin/customParseFormat' import customParseFormat from 'dayjs/plugin/customParseFormat'
import timezone from 'dayjs/plugin/timezone' import timezone from 'dayjs/plugin/timezone'
import utc from 'dayjs/plugin/utc' import utc from 'dayjs/plugin/utc'
import { keyBy, mapValues, sortBy, throttle } from 'lodash' import { keyBy, mapValues, sortBy } from 'lodash'
import Image, { ImageProps, StaticImageData } from 'next/image' import Image, { ImageProps, StaticImageData } from 'next/image'
import Link from 'next/link' import Link from 'next/link'
import { ReactNode, useEffect, useRef, useState } from 'react' import { useState } from 'react'
import { ContractCard } from 'web/components/contract/contract-card' import { ContractCard } from 'web/components/contract/contract-card'
import { DateTimeTooltip } from 'web/components/datetime-tooltip' import { DateTimeTooltip } from 'web/components/datetime-tooltip'
import { Col } from 'web/components/layout/col' import { Col } from 'web/components/layout/col'
@ -33,6 +28,7 @@ import mpox_pic from './_cspi/Monkeypox_Cases.png'
import race_pic from './_cspi/Supreme_Court_Ban_Race_in_College_Admissions.png' import race_pic from './_cspi/Supreme_Court_Ban_Race_in_College_Admissions.png'
import { SiteLink } from 'web/components/site-link' import { SiteLink } from 'web/components/site-link'
import { getProbability } from 'common/calculate' import { getProbability } from 'common/calculate'
import { Carousel } from 'web/components/carousel'
dayjs.extend(utc) dayjs.extend(utc)
dayjs.extend(timezone) dayjs.extend(timezone)
@ -254,58 +250,3 @@ const NaturalImage = (props: ImageProps) => {
/> />
) )
} }
function Carousel(props: { children: ReactNode; className?: string }) {
const { children, className } = props
const ref = useRef<HTMLDivElement>(null)
const th = (f: () => any) => throttle(f, 500, { trailing: false })
const scrollLeft = th(() =>
ref.current?.scrollBy({ left: -ref.current.clientWidth })
)
const scrollRight = th(() =>
ref.current?.scrollBy({ left: ref.current.clientWidth })
)
const [atFront, setAtFront] = useState(true)
const [atBack, setAtBack] = useState(false)
const onScroll = throttle(() => {
if (ref.current) {
const { scrollLeft, clientWidth, scrollWidth } = ref.current
setAtFront(scrollLeft < 80)
setAtBack(scrollWidth - (clientWidth + scrollLeft) < 80)
}
}, 500)
// eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(onScroll, [])
return (
<div className={clsx('relative', className)}>
<Row
className="scrollbar-hide w-full gap-4 overflow-x-auto scroll-smooth"
ref={ref}
onScroll={onScroll}
>
{children}
</Row>
{!atFront && (
<div
className="absolute left-0 top-0 bottom-0 z-10 flex w-10 cursor-pointer items-center justify-center hover:bg-indigo-100/30"
onMouseDown={scrollLeft}
>
<ChevronLeftIcon className="h-7 w-7 rounded-full bg-indigo-50 text-indigo-700" />
</div>
)}
{!atBack && (
<div
className="absolute right-0 top-0 bottom-0 z-10 flex w-10 cursor-pointer items-center justify-center hover:bg-indigo-100/30"
onMouseDown={scrollRight}
>
<ChevronRightIcon className="h-7 w-7 rounded-full bg-indigo-50 text-indigo-700" />
</div>
)}
</div>
)
}