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'
import { ShowTime } from './contract/contract-details'
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 { useFollows } from 'web/hooks/use-follows'
import {
@ -85,6 +85,7 @@ export function ContractSearch(props: {
isWholePage?: boolean
maxItems?: number
noControls?: boolean
renderContracts?: (contracts: Contract[] | undefined) => ReactNode
}) {
const {
user,
@ -101,6 +102,7 @@ export function ContractSearch(props: {
isWholePage,
maxItems,
noControls,
renderContracts,
} = props
const [state, setState] = usePersistentState(
@ -203,6 +205,9 @@ export function ContractSearch(props: {
onSearchParametersChanged={onSearchParametersChanged}
noControls={noControls}
/>
{renderContracts ? (
renderContracts(renderedContracts)
) : (
<ContractsGrid
contracts={renderedContracts}
loadMore={noControls ? undefined : performQuery}
@ -211,6 +216,7 @@ export function ContractSearch(props: {
highlightOptions={highlightOptions}
cardHideOptions={cardHideOptions}
/>
)}
</Col>
)
}

View File

@ -1,5 +1,5 @@
import React from 'react'
import { useRouter } from 'next/router'
import Router from 'next/router'
import { PlusSmIcon } from '@heroicons/react/solid'
import { Page } from 'web/components/page'
@ -17,7 +17,13 @@ import { Button } from 'web/components/button'
import { Spacer } from 'web/components/layout/spacer'
import { useMemberGroups } from 'web/hooks/use-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) => {
const creds = await authenticateOnServer(ctx)
@ -28,7 +34,6 @@ export const getServerSideProps: GetServerSideProps = async (ctx) => {
const Home = (props: { auth: { user: User } | null }) => {
const user = props.auth ? props.auth.user : null
const router = useRouter()
useTracking('view home')
useSaveReferral()
@ -39,7 +44,7 @@ const Home = (props: { auth: { user: User } | null }) => {
return (
<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="Newest" sort="newest" user={user} />
<SearchSection label="Closing soon" sort="close-date" user={user} />
@ -51,7 +56,7 @@ const Home = (props: { auth: { user: User } | null }) => {
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"
onClick={() => {
router.push('/create')
Router.push('/create')
track('mobile create button')
}}
>
@ -68,21 +73,25 @@ function SearchSection(props: {
}) {
const { label, user, sort } = props
const router = useRouter()
return (
<Col>
<Title className="mx-2 !text-gray-800 sm:mx-0" text={label} />
<Spacer h={2} />
<ContractSearch user={user} defaultSort={sort} maxItems={4} noControls />
<Button
className="self-end"
color="blue"
size="sm"
onClick={() => router.push(`/home?s=${sort}`)}
>
See more
</Button>
<Subtitle className="mx-2 !mt-2 !text-gray-800 sm:mx-0" text={label} />
<ContractSearch
user={user}
defaultSort={sort}
maxItems={12}
noControls
renderContracts={(contracts) =>
contracts ? (
<DoubleCarousel
contracts={contracts}
seeMoreUrl={`/home?s=${sort}`}
/>
) : (
<LoadingIndicator />
)
}
/>
</Col>
)
}
@ -90,29 +99,74 @@ function SearchSection(props: {
function GroupSection(props: { group: Group; user: User | null }) {
const { group, user } = props
const router = useRouter()
return (
<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} />
<ContractSearch
user={user}
defaultSort={'score'}
additionalFilter={{ groupSlug: group.slug }}
maxItems={4}
maxItems={12}
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>
)
}
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

View File

@ -1,10 +1,5 @@
import { ClockIcon } from '@heroicons/react/outline'
import {
ChevronLeftIcon,
ChevronRightIcon,
UsersIcon,
} from '@heroicons/react/solid'
import clsx from 'clsx'
import { UsersIcon } from '@heroicons/react/solid'
import {
BinaryContract,
Contract,
@ -15,10 +10,10 @@ import dayjs, { Dayjs } from 'dayjs'
import customParseFormat from 'dayjs/plugin/customParseFormat'
import timezone from 'dayjs/plugin/timezone'
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 Link from 'next/link'
import { ReactNode, useEffect, useRef, useState } from 'react'
import { useState } from 'react'
import { ContractCard } from 'web/components/contract/contract-card'
import { DateTimeTooltip } from 'web/components/datetime-tooltip'
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 { SiteLink } from 'web/components/site-link'
import { getProbability } from 'common/calculate'
import { Carousel } from 'web/components/carousel'
dayjs.extend(utc)
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>
)
}