Implement double carousel for /experimental/home
This commit is contained in:
parent
d658a48b66
commit
f83b62cf50
60
web/components/carousel.tsx
Normal file
60
web/components/carousel.tsx
Normal 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>
|
||||
)
|
||||
}
|
|
@ -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,14 +205,18 @@ export function ContractSearch(props: {
|
|||
onSearchParametersChanged={onSearchParametersChanged}
|
||||
noControls={noControls}
|
||||
/>
|
||||
<ContractsGrid
|
||||
contracts={renderedContracts}
|
||||
loadMore={noControls ? undefined : performQuery}
|
||||
showTime={state.showTime ?? undefined}
|
||||
onContractClick={onContractClick}
|
||||
highlightOptions={highlightOptions}
|
||||
cardHideOptions={cardHideOptions}
|
||||
/>
|
||||
{renderContracts ? (
|
||||
renderContracts(renderedContracts)
|
||||
) : (
|
||||
<ContractsGrid
|
||||
contracts={renderedContracts}
|
||||
loadMore={noControls ? undefined : performQuery}
|
||||
showTime={state.showTime ?? undefined}
|
||||
onContractClick={onContractClick}
|
||||
highlightOptions={highlightOptions}
|
||||
cardHideOptions={cardHideOptions}
|
||||
/>
|
||||
)}
|
||||
</Col>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user