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'
|
} 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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user