Allow adding multiple contracts to group in modal

This commit is contained in:
Ian Philips 2022-07-25 18:27:43 -07:00
parent ec0e25e5ed
commit af25a6c795
8 changed files with 171 additions and 77 deletions

View File

@ -15,7 +15,10 @@ import {
useInitialQueryAndSort, useInitialQueryAndSort,
useUpdateQueryAndSort, useUpdateQueryAndSort,
} from '../hooks/use-sort-and-query-params' } from '../hooks/use-sort-and-query-params'
import { ContractsGrid } from './contract/contracts-list' import {
ContractHighlightOptions,
ContractsGrid,
} from './contract/contracts-list'
import { Row } from './layout/row' import { Row } from './layout/row'
import { useEffect, useMemo, useRef, useState } from 'react' import { useEffect, useMemo, useRef, useState } from 'react'
import { Spacer } from './layout/spacer' import { Spacer } from './layout/spacer'
@ -64,11 +67,15 @@ export function ContractSearch(props: {
excludeContractIds?: string[] excludeContractIds?: string[]
groupSlug?: string groupSlug?: string
} }
highlightOptions?: ContractHighlightOptions
onContractClick?: (contract: Contract) => void onContractClick?: (contract: Contract) => void
showPlaceHolder?: boolean showPlaceHolder?: boolean
hideOrderSelector?: boolean hideOrderSelector?: boolean
overrideGridClassName?: string overrideGridClassName?: string
hideQuickBet?: boolean cardHideOptions?: {
hideGroupLink?: boolean
hideQuickBet?: boolean
}
}) { }) {
const { const {
querySortOptions, querySortOptions,
@ -77,7 +84,8 @@ export function ContractSearch(props: {
overrideGridClassName, overrideGridClassName,
hideOrderSelector, hideOrderSelector,
showPlaceHolder, showPlaceHolder,
hideQuickBet, cardHideOptions,
highlightOptions,
} = props } = props
const user = useUser() const user = useUser()
@ -276,8 +284,9 @@ export function ContractSearch(props: {
querySortOptions={querySortOptions} querySortOptions={querySortOptions}
onContractClick={onContractClick} onContractClick={onContractClick}
overrideGridClassName={overrideGridClassName} overrideGridClassName={overrideGridClassName}
hideQuickBet={hideQuickBet}
excludeContractIds={additionalFilter?.excludeContractIds} excludeContractIds={additionalFilter?.excludeContractIds}
highlightOptions={highlightOptions}
cardHideOptions={cardHideOptions}
/> />
)} )}
</InstantSearch> </InstantSearch>
@ -293,13 +302,19 @@ export function ContractSearchInner(props: {
overrideGridClassName?: string overrideGridClassName?: string
hideQuickBet?: boolean hideQuickBet?: boolean
excludeContractIds?: string[] excludeContractIds?: string[]
highlightOptions?: ContractHighlightOptions
cardHideOptions?: {
hideQuickBet?: boolean
hideGroupLink?: boolean
}
}) { }) {
const { const {
querySortOptions, querySortOptions,
onContractClick, onContractClick,
overrideGridClassName, overrideGridClassName,
hideQuickBet, cardHideOptions,
excludeContractIds, excludeContractIds,
highlightOptions,
} = props } = props
const { initialQuery } = useInitialQueryAndSort(querySortOptions) const { initialQuery } = useInitialQueryAndSort(querySortOptions)
@ -360,7 +375,8 @@ export function ContractSearchInner(props: {
showTime={showTime} showTime={showTime}
onContractClick={onContractClick} onContractClick={onContractClick}
overrideGridClassName={overrideGridClassName} overrideGridClassName={overrideGridClassName}
hideQuickBet={hideQuickBet} highlightOptions={highlightOptions}
cardHideOptions={cardHideOptions}
/> />
) )
} }

View File

@ -5,8 +5,8 @@ import { formatLargeNumber, formatPercent } from 'common/util/format'
import { contractPath, getBinaryProbPercent } from 'web/lib/firebase/contracts' import { contractPath, getBinaryProbPercent } from 'web/lib/firebase/contracts'
import { Col } from '../layout/col' import { Col } from '../layout/col'
import { import {
Contract,
BinaryContract, BinaryContract,
Contract,
FreeResponseContract, FreeResponseContract,
NumericContract, NumericContract,
PseudoNumericContract, PseudoNumericContract,
@ -24,7 +24,7 @@ import {
} from 'common/calculate' } from 'common/calculate'
import { AvatarDetails, MiscDetails, ShowTime } from './contract-details' import { AvatarDetails, MiscDetails, ShowTime } from './contract-details'
import { getExpectedValue, getValueFromBucket } from 'common/calculate-dpm' import { getExpectedValue, getValueFromBucket } from 'common/calculate-dpm'
import { QuickBet, ProbBar, getColor } from './quick-bet' import { getColor, ProbBar, QuickBet } from './quick-bet'
import { useContractWithPreload } from 'web/hooks/use-contract' import { useContractWithPreload } from 'web/hooks/use-contract'
import { useUser } from 'web/hooks/use-user' import { useUser } from 'web/hooks/use-user'
import { track } from '@amplitude/analytics-browser' import { track } from '@amplitude/analytics-browser'
@ -38,8 +38,16 @@ export function ContractCard(props: {
className?: string className?: string
onClick?: () => void onClick?: () => void
hideQuickBet?: boolean hideQuickBet?: boolean
hideGroupLink?: boolean
}) { }) {
const { showHotVolume, showTime, className, onClick, hideQuickBet } = props const {
showHotVolume,
showTime,
className,
onClick,
hideQuickBet,
hideGroupLink,
} = props
const contract = useContractWithPreload(props.contract) ?? props.contract const contract = useContractWithPreload(props.contract) ?? props.contract
const { question, outcomeType } = contract const { question, outcomeType } = contract
const { resolution } = contract const { resolution } = contract
@ -121,6 +129,7 @@ export function ContractCard(props: {
contract={contract} contract={contract}
showHotVolume={showHotVolume} showHotVolume={showHotVolume}
showTime={showTime} showTime={showTime}
hideGroupLink={hideGroupLink}
/> />
</Col> </Col>
{showQuickBet ? ( {showQuickBet ? (

View File

@ -42,8 +42,9 @@ export function MiscDetails(props: {
contract: Contract contract: Contract
showHotVolume?: boolean showHotVolume?: boolean
showTime?: ShowTime showTime?: ShowTime
hideGroupLink?: boolean
}) { }) {
const { contract, showHotVolume, showTime } = props const { contract, showHotVolume, showTime, hideGroupLink } = props
const { const {
volume, volume,
volume24Hours, volume24Hours,
@ -80,7 +81,7 @@ export function MiscDetails(props: {
<NewContractBadge /> <NewContractBadge />
)} )}
{groupLinks && groupLinks.length > 0 && ( {!hideGroupLink && groupLinks && groupLinks.length > 0 && (
<SiteLink <SiteLink
href={groupPath(groupLinks[0].slug)} href={groupPath(groupLinks[0].slug)}
className="text-sm text-gray-400" className="text-sm text-gray-400"

View File

@ -1,5 +1,5 @@
import { Contract } from '../../lib/firebase/contracts' import { Contract } from 'web/lib/firebase/contracts'
import { User } from '../../lib/firebase/users' import { User } from 'web/lib/firebase/users'
import { Col } from '../layout/col' import { Col } from '../layout/col'
import { SiteLink } from '../site-link' import { SiteLink } from '../site-link'
import { ContractCard } from './contract-card' import { ContractCard } from './contract-card'
@ -9,6 +9,11 @@ import { useIsVisible } from 'web/hooks/use-is-visible'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import clsx from 'clsx' import clsx from 'clsx'
export type ContractHighlightOptions = {
contractIds?: string[]
highlightClassName?: string
}
export function ContractsGrid(props: { export function ContractsGrid(props: {
contracts: Contract[] contracts: Contract[]
loadMore: () => void loadMore: () => void
@ -16,7 +21,11 @@ export function ContractsGrid(props: {
showTime?: ShowTime showTime?: ShowTime
onContractClick?: (contract: Contract) => void onContractClick?: (contract: Contract) => void
overrideGridClassName?: string overrideGridClassName?: string
hideQuickBet?: boolean cardHideOptions?: {
hideQuickBet?: boolean
hideGroupLink?: boolean
}
highlightOptions?: ContractHighlightOptions
}) { }) {
const { const {
contracts, contracts,
@ -25,9 +34,12 @@ export function ContractsGrid(props: {
loadMore, loadMore,
onContractClick, onContractClick,
overrideGridClassName, overrideGridClassName,
hideQuickBet, cardHideOptions,
highlightOptions,
} = props } = props
const { hideQuickBet, hideGroupLink } = cardHideOptions || {}
const { contractIds, highlightClassName } = highlightOptions || {}
const [elem, setElem] = useState<HTMLElement | null>(null) const [elem, setElem] = useState<HTMLElement | null>(null)
const isBottomVisible = useIsVisible(elem) const isBottomVisible = useIsVisible(elem)
@ -66,6 +78,12 @@ export function ContractsGrid(props: {
onContractClick ? () => onContractClick(contract) : undefined onContractClick ? () => onContractClick(contract) : undefined
} }
hideQuickBet={hideQuickBet} hideQuickBet={hideQuickBet}
hideGroupLink={hideGroupLink}
className={
contractIds?.includes(contract.id)
? highlightClassName
: undefined
}
/> />
))} ))}
</ul> </ul>

View File

@ -26,7 +26,7 @@ export function Modal(props: {
className="fixed inset-0 z-50 overflow-y-auto" className="fixed inset-0 z-50 overflow-y-auto"
onClose={setOpen} onClose={setOpen}
> >
<div className="flex min-h-screen items-end justify-center px-4 pt-4 pb-20 text-center sm:block sm:p-0"> <div className="flex min-h-screen items-end justify-center px-4 pt-4 pb-20 text-center sm:p-0">
<Transition.Child <Transition.Child
as={Fragment} as={Fragment}
enter="ease-out duration-300" enter="ease-out duration-300"

View File

@ -129,7 +129,6 @@ export async function listContractsByGroupSlug(
): Promise<Contract[]> { ): Promise<Contract[]> {
const q = query(contracts, where('groupSlugs', 'array-contains', slug)) const q = query(contracts, where('groupSlugs', 'array-contains', slug))
const snapshot = await getDocs(q) const snapshot = await getDocs(q)
console.log(snapshot.docs.map((doc) => doc.data()))
return snapshot.docs.map((doc) => doc.data()) return snapshot.docs.map((doc) => doc.data())
} }

View File

@ -129,56 +129,61 @@ export async function addContractToGroup(
contract: Contract, contract: Contract,
userId: string userId: string
) { ) {
if (contract.groupLinks?.map((l) => l.groupId).includes(group.id)) return // already in that group if (!contract.groupLinks?.map((l) => l.groupId).includes(group.id)) {
const newGroupLinks = [
...(contract.groupLinks ?? []),
{
groupId: group.id,
createdTime: Date.now(),
slug: group.slug,
userId,
name: group.name,
} as GroupLink,
]
const newGroupLinks = [ await updateContract(contract.id, {
...(contract.groupLinks ?? []), groupSlugs: uniq([...(contract.groupSlugs ?? []), group.slug]),
{ groupLinks: newGroupLinks,
groupId: group.id,
createdTime: Date.now(),
slug: group.slug,
userId,
name: group.name,
} as GroupLink,
]
await updateContract(contract.id, {
groupSlugs: uniq([...(contract.groupSlugs ?? []), group.slug]),
groupLinks: newGroupLinks,
})
return await updateGroup(group, {
contractIds: uniq([...group.contractIds, contract.id]),
})
.then(() => group)
.catch((err) => {
console.error('error adding contract to group', err)
return err
}) })
}
if (!group.contractIds.includes(contract.id)) {
return await updateGroup(group, {
contractIds: uniq([...group.contractIds, contract.id]),
})
.then(() => group)
.catch((err) => {
console.error('error adding contract to group', err)
return err
})
}
} }
export async function removeContractFromGroup( export async function removeContractFromGroup(
group: Group, group: Group,
contract: Contract contract: Contract
) { ) {
if (!contract.groupLinks?.map((l) => l.groupId).includes(group.id)) return // not in that group if (contract.groupLinks?.map((l) => l.groupId).includes(group.id)) {
const newGroupLinks = contract.groupLinks?.filter(
const newGroupLinks = contract.groupLinks?.filter( (link) => link.slug !== group.slug
(link) => link.slug !== group.slug )
) await updateContract(contract.id, {
await updateContract(contract.id, { groupSlugs:
groupSlugs: contract.groupSlugs?.filter((slug) => slug !== group.slug) ?? [],
contract.groupSlugs?.filter((slug) => slug !== group.slug) ?? [], groupLinks: newGroupLinks ?? [],
groupLinks: newGroupLinks ?? [],
})
const newContractIds = group.contractIds.filter((id) => id !== contract.id)
return await updateGroup(group, {
contractIds: uniq(newContractIds),
})
.then(() => group)
.catch((err) => {
console.error('error removing contract from group', err)
return err
}) })
}
if (group.contractIds.includes(contract.id)) {
const newContractIds = group.contractIds.filter((id) => id !== contract.id)
return await updateGroup(group, {
contractIds: uniq(newContractIds),
})
.then(() => group)
.catch((err) => {
console.error('error removing contract from group', err)
return err
})
}
} }
export async function setContractGroupLinks( export async function setContractGroupLinks(

View File

@ -49,6 +49,7 @@ import { useWindowSize } from 'web/hooks/use-window-size'
import { CopyLinkButton } from 'web/components/copy-link-button' import { CopyLinkButton } from 'web/components/copy-link-button'
import { ENV_CONFIG } from 'common/envs/constants' import { ENV_CONFIG } from 'common/envs/constants'
import { useSaveReferral } from 'web/hooks/use-save-referral' import { useSaveReferral } from 'web/hooks/use-save-referral'
import { Button } from 'web/components/button'
export const getStaticProps = fromPropz(getStaticPropz) export const getStaticProps = fromPropz(getStaticPropz)
export async function getStaticPropz(props: { params: { slugs: string[] } }) { export async function getStaticPropz(props: { params: { slugs: string[] } }) {
@ -541,10 +542,26 @@ function GroupLeaderboards(props: {
function AddContractButton(props: { group: Group; user: User }) { function AddContractButton(props: { group: Group; user: User }) {
const { group, user } = props const { group, user } = props
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
const [contracts, setContracts] = useState<Contract[]>([])
const [loading, setLoading] = useState(false)
async function addContractToCurrentGroup(contract: Contract) { async function addContractToCurrentGroup(contract: Contract) {
await addContractToGroup(group, contract, user.id) if (contracts.map((c) => c.id).includes(contract.id)) {
setOpen(false) setContracts(contracts.filter((c) => c.id !== contract.id))
} else setContracts([...contracts, contract])
}
async function doneAddingContracts() {
Promise.all(
contracts.map(async (contract) => {
setLoading(true)
await addContractToGroup(group, contract, user.id)
})
).then(() => {
setLoading(false)
setOpen(false)
setContracts([])
})
} }
return ( return (
@ -558,37 +575,66 @@ function AddContractButton(props: { group: Group; user: User }) {
</button> </button>
</div> </div>
<Modal open={open} setOpen={setOpen} className={'sm:p-0'}> <Modal open={open} setOpen={setOpen} className={'sm:p-0'} size={'lg'}>
<Col <Col className={' w-full gap-4 rounded-md bg-white'}>
className={
'max-h-[60vh] min-h-[60vh] w-full gap-4 rounded-md bg-white'
}
>
<Col className="p-8 pb-0"> <Col className="p-8 pb-0">
<div className={'text-xl text-indigo-700'}> <div className={'text-xl text-indigo-700'}>
Add a question to your group Add a question to your group
</div> </div>
<Col className="items-center"> {contracts.length === 0 ? (
<CreateQuestionButton <Col className="items-center justify-center">
user={user} <CreateQuestionButton
overrideText={'New question'} user={user}
className={'w-48 flex-shrink-0 '} overrideText={'New question'}
query={`?groupId=${group.id}`} className={'w-48 flex-shrink-0 '}
/> query={`?groupId=${group.id}`}
/>
<div className={'mt-2 text-lg text-indigo-700'}>or</div> <div className={'mt-1 text-lg text-gray-600'}>
</Col> (or select old questions)
</div>
</Col>
) : (
<Col className={'w-full '}>
{!loading ? (
<Row className={'justify-end gap-4'}>
<Button onClick={doneAddingContracts} color={'indigo'}>
Add {contracts.length} question
{contracts.length > 1 && 's'}
</Button>
<Button
onClick={() => {
setContracts([])
}}
color={'gray'}
>
Cancel
</Button>
</Row>
) : (
<Row className={'justify-center'}>
<LoadingIndicator />
</Row>
)}
</Col>
)}
</Col> </Col>
<div className={'overflow-y-scroll sm:px-8'}> <div className={'overflow-y-scroll sm:px-8'}>
<ContractSearch <ContractSearch
hideOrderSelector={true} hideOrderSelector={true}
onContractClick={addContractToCurrentGroup} onContractClick={addContractToCurrentGroup}
overrideGridClassName={'flex grid-cols-1 flex-col gap-3 p-1'} overrideGridClassName={
'flex grid grid-cols-1 sm:grid-cols-2 flex-col gap-3 p-1'
}
showPlaceHolder={true} showPlaceHolder={true}
hideQuickBet={true} cardHideOptions={{ hideGroupLink: true, hideQuickBet: true }}
additionalFilter={{ excludeContractIds: group.contractIds }} additionalFilter={{ excludeContractIds: group.contractIds }}
highlightOptions={{
contractIds: contracts.map((c) => c.id),
highlightClassName: '!bg-indigo-100 border-indigo-100 border-2',
}}
/> />
</div> </div>
</Col> </Col>