new market view (#819)
* Show old details on lg, don't unfill heart * Hide tip market if creator * Small ui tweaks * Remove contract. calls * Update high-medium-low * Remove unused bets prop * Show uniques * Remove unused bets prop
This commit is contained in:
parent
3e1e84ee5e
commit
aad5f6528b
|
@ -3,6 +3,6 @@ export type Like = {
|
||||||
userId: string
|
userId: string
|
||||||
type: 'contract'
|
type: 'contract'
|
||||||
createdTime: number
|
createdTime: number
|
||||||
tipTxnId?: string
|
tipTxnId?: string // only holds most recent tip txn id
|
||||||
}
|
}
|
||||||
export const LIKE_TIP_AMOUNT = 5
|
export const LIKE_TIP_AMOUNT = 5
|
||||||
|
|
|
@ -31,8 +31,7 @@ export * from './weekly-markets-emails'
|
||||||
export * from './reset-betting-streaks'
|
export * from './reset-betting-streaks'
|
||||||
export * from './reset-weekly-emails-flag'
|
export * from './reset-weekly-emails-flag'
|
||||||
export * from './on-update-contract-follow'
|
export * from './on-update-contract-follow'
|
||||||
export * from './on-create-like'
|
export * from './on-update-like'
|
||||||
export * from './on-delete-like'
|
|
||||||
|
|
||||||
// v2
|
// v2
|
||||||
export * from './health'
|
export * from './health'
|
||||||
|
|
|
@ -1,32 +0,0 @@
|
||||||
import * as functions from 'firebase-functions'
|
|
||||||
import * as admin from 'firebase-admin'
|
|
||||||
import { Like } from '../../common/like'
|
|
||||||
import { getContract, log } from './utils'
|
|
||||||
import { uniq } from 'lodash'
|
|
||||||
|
|
||||||
const firestore = admin.firestore()
|
|
||||||
|
|
||||||
export const onDeleteLike = functions.firestore
|
|
||||||
.document('users/{userId}/likes/{likeId}')
|
|
||||||
.onDelete(async (change) => {
|
|
||||||
const like = change.data() as Like
|
|
||||||
if (like.type === 'contract') {
|
|
||||||
await removeContractLike(like)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const removeContractLike = async (like: Like) => {
|
|
||||||
const contract = await getContract(like.id)
|
|
||||||
if (!contract) {
|
|
||||||
log('Could not find contract')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const likedByUserIds = uniq(contract.likedByUserIds ?? [])
|
|
||||||
const newLikedByUserIds = likedByUserIds.filter(
|
|
||||||
(userId) => userId !== like.userId
|
|
||||||
)
|
|
||||||
await firestore.collection('contracts').doc(like.id).update({
|
|
||||||
likedByUserIds: newLikedByUserIds,
|
|
||||||
likedByUserCount: newLikedByUserIds.length,
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -19,14 +19,36 @@ export const onCreateLike = functions.firestore
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const onUpdateLike = functions.firestore
|
||||||
|
.document('users/{userId}/likes/{likeId}')
|
||||||
|
.onUpdate(async (change, context) => {
|
||||||
|
const like = change.after.data() as Like
|
||||||
|
const prevLike = change.before.data() as Like
|
||||||
|
const { eventId } = context
|
||||||
|
if (like.type === 'contract' && like.tipTxnId !== prevLike.tipTxnId) {
|
||||||
|
await handleCreateLikeNotification(like, eventId)
|
||||||
|
await updateContractLikes(like)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export const onDeleteLike = functions.firestore
|
||||||
|
.document('users/{userId}/likes/{likeId}')
|
||||||
|
.onDelete(async (change) => {
|
||||||
|
const like = change.data() as Like
|
||||||
|
if (like.type === 'contract') {
|
||||||
|
await removeContractLike(like)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const updateContractLikes = async (like: Like) => {
|
const updateContractLikes = async (like: Like) => {
|
||||||
const contract = await getContract(like.id)
|
const contract = await getContract(like.id)
|
||||||
if (!contract) {
|
if (!contract) {
|
||||||
log('Could not find contract')
|
log('Could not find contract')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const likedByUserIds = uniq(contract.likedByUserIds ?? [])
|
const likedByUserIds = uniq(
|
||||||
likedByUserIds.push(like.userId)
|
(contract.likedByUserIds ?? []).concat(like.userId)
|
||||||
|
)
|
||||||
await firestore
|
await firestore
|
||||||
.collection('contracts')
|
.collection('contracts')
|
||||||
.doc(like.id)
|
.doc(like.id)
|
||||||
|
@ -69,3 +91,19 @@ const handleCreateLikeNotification = async (like: Like, eventId: string) => {
|
||||||
tipTxnData
|
tipTxnData
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const removeContractLike = async (like: Like) => {
|
||||||
|
const contract = await getContract(like.id)
|
||||||
|
if (!contract) {
|
||||||
|
log('Could not find contract')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const likedByUserIds = uniq(contract.likedByUserIds ?? [])
|
||||||
|
const newLikedByUserIds = likedByUserIds.filter(
|
||||||
|
(userId) => userId !== like.userId
|
||||||
|
)
|
||||||
|
await firestore.collection('contracts').doc(like.id).update({
|
||||||
|
likedByUserIds: newLikedByUserIds,
|
||||||
|
likedByUserCount: newLikedByUserIds.length,
|
||||||
|
})
|
||||||
|
}
|
|
@ -18,7 +18,6 @@ import { fromNow } from 'web/lib/util/time'
|
||||||
import { Avatar } from '../avatar'
|
import { Avatar } from '../avatar'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { ContractInfoDialog } from './contract-info-dialog'
|
import { ContractInfoDialog } from './contract-info-dialog'
|
||||||
import { Bet } from 'common/bet'
|
|
||||||
import NewContractBadge from '../new-contract-badge'
|
import NewContractBadge from '../new-contract-badge'
|
||||||
import { UserFollowButton } from '../follow-button'
|
import { UserFollowButton } from '../follow-button'
|
||||||
import { DAY_MS } from 'common/util/time'
|
import { DAY_MS } from 'common/util/time'
|
||||||
|
@ -35,6 +34,8 @@ import { contractMetrics } from 'common/contract-details'
|
||||||
import { User } from 'common/user'
|
import { User } from 'common/user'
|
||||||
import { UserLink } from 'web/components/user-link'
|
import { UserLink } from 'web/components/user-link'
|
||||||
import { FeaturedContractBadge } from 'web/components/contract/featured-contract-badge'
|
import { FeaturedContractBadge } from 'web/components/contract/featured-contract-badge'
|
||||||
|
import { Tooltip } from 'web/components/tooltip'
|
||||||
|
import { useWindowSize } from 'web/hooks/use-window-size'
|
||||||
|
|
||||||
export type ShowTime = 'resolve-date' | 'close-date'
|
export type ShowTime = 'resolve-date' | 'close-date'
|
||||||
|
|
||||||
|
@ -78,7 +79,7 @@ export function MiscDetails(props: {
|
||||||
) : (contract?.featuredOnHomeRank ?? 0) > 0 ? (
|
) : (contract?.featuredOnHomeRank ?? 0) > 0 ? (
|
||||||
<FeaturedContractBadge />
|
<FeaturedContractBadge />
|
||||||
) : volume > 0 || !isNew ? (
|
) : volume > 0 || !isNew ? (
|
||||||
<Row className={'shrink-0'}>{formatMoney(contract.volume)} bet</Row>
|
<Row className={'shrink-0'}>{formatMoney(volume)} bet</Row>
|
||||||
) : (
|
) : (
|
||||||
<NewContractBadge />
|
<NewContractBadge />
|
||||||
)}
|
)}
|
||||||
|
@ -101,7 +102,7 @@ export function AvatarDetails(props: {
|
||||||
short?: boolean
|
short?: boolean
|
||||||
}) {
|
}) {
|
||||||
const { contract, short, className } = props
|
const { contract, short, className } = props
|
||||||
const { creatorName, creatorUsername } = contract
|
const { creatorName, creatorUsername, creatorAvatarUrl } = contract
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Row
|
<Row
|
||||||
|
@ -109,7 +110,7 @@ export function AvatarDetails(props: {
|
||||||
>
|
>
|
||||||
<Avatar
|
<Avatar
|
||||||
username={creatorUsername}
|
username={creatorUsername}
|
||||||
avatarUrl={contract.creatorAvatarUrl}
|
avatarUrl={creatorAvatarUrl}
|
||||||
size={6}
|
size={6}
|
||||||
/>
|
/>
|
||||||
<UserLink name={creatorName} username={creatorUsername} short={short} />
|
<UserLink name={creatorName} username={creatorUsername} short={short} />
|
||||||
|
@ -138,20 +139,28 @@ export function AbbrContractDetails(props: {
|
||||||
|
|
||||||
export function ContractDetails(props: {
|
export function ContractDetails(props: {
|
||||||
contract: Contract
|
contract: Contract
|
||||||
bets: Bet[]
|
|
||||||
user: User | null | undefined
|
user: User | null | undefined
|
||||||
isCreator?: boolean
|
isCreator?: boolean
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
}) {
|
}) {
|
||||||
const { contract, bets, isCreator, disabled } = props
|
const { contract, isCreator, disabled } = props
|
||||||
const { closeTime, creatorName, creatorUsername, creatorId, groupLinks } =
|
const {
|
||||||
contract
|
closeTime,
|
||||||
|
creatorName,
|
||||||
|
creatorUsername,
|
||||||
|
creatorId,
|
||||||
|
groupLinks,
|
||||||
|
creatorAvatarUrl,
|
||||||
|
resolutionTime,
|
||||||
|
} = contract
|
||||||
const { volumeLabel, resolvedDate } = contractMetrics(contract)
|
const { volumeLabel, resolvedDate } = contractMetrics(contract)
|
||||||
|
|
||||||
const groupToDisplay =
|
const groupToDisplay =
|
||||||
groupLinks?.sort((a, b) => a.createdTime - b.createdTime)[0] ?? null
|
groupLinks?.sort((a, b) => a.createdTime - b.createdTime)[0] ?? null
|
||||||
const user = useUser()
|
const user = useUser()
|
||||||
const [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
|
const { width } = useWindowSize()
|
||||||
|
const isMobile = (width ?? 0) < 600
|
||||||
|
|
||||||
const groupInfo = (
|
const groupInfo = (
|
||||||
<Row>
|
<Row>
|
||||||
|
@ -167,7 +176,7 @@ export function ContractDetails(props: {
|
||||||
<Row className="items-center gap-2">
|
<Row className="items-center gap-2">
|
||||||
<Avatar
|
<Avatar
|
||||||
username={creatorUsername}
|
username={creatorUsername}
|
||||||
avatarUrl={contract.creatorAvatarUrl}
|
avatarUrl={creatorAvatarUrl}
|
||||||
noLink={disabled}
|
noLink={disabled}
|
||||||
size={6}
|
size={6}
|
||||||
/>
|
/>
|
||||||
|
@ -178,6 +187,7 @@ export function ContractDetails(props: {
|
||||||
className="whitespace-nowrap"
|
className="whitespace-nowrap"
|
||||||
name={creatorName}
|
name={creatorName}
|
||||||
username={creatorUsername}
|
username={creatorUsername}
|
||||||
|
short={isMobile}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{!disabled && <UserFollowButton userId={creatorId} small />}
|
{!disabled && <UserFollowButton userId={creatorId} small />}
|
||||||
|
@ -228,14 +238,11 @@ export function ContractDetails(props: {
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
{(!!closeTime || !!resolvedDate) && (
|
{(!!closeTime || !!resolvedDate) && (
|
||||||
<Row className="items-center gap-1">
|
<Row className="hidden items-center gap-1 md:inline-flex">
|
||||||
{resolvedDate && contract.resolutionTime ? (
|
{resolvedDate && resolutionTime ? (
|
||||||
<>
|
<>
|
||||||
<ClockIcon className="h-5 w-5" />
|
<ClockIcon className="h-5 w-5" />
|
||||||
<DateTimeTooltip
|
<DateTimeTooltip text="Market resolved:" time={resolutionTime}>
|
||||||
text="Market resolved:"
|
|
||||||
time={contract.resolutionTime}
|
|
||||||
>
|
|
||||||
{resolvedDate}
|
{resolvedDate}
|
||||||
</DateTimeTooltip>
|
</DateTimeTooltip>
|
||||||
</>
|
</>
|
||||||
|
@ -255,17 +262,84 @@ export function ContractDetails(props: {
|
||||||
)}
|
)}
|
||||||
{user && (
|
{user && (
|
||||||
<>
|
<>
|
||||||
<Row className="items-center gap-1">
|
<Row className="hidden items-center gap-1 md:inline-flex">
|
||||||
<DatabaseIcon className="h-5 w-5" />
|
<DatabaseIcon className="h-5 w-5" />
|
||||||
<div className="whitespace-nowrap">{volumeLabel}</div>
|
<div className="whitespace-nowrap">{volumeLabel}</div>
|
||||||
</Row>
|
</Row>
|
||||||
{!disabled && <ContractInfoDialog contract={contract} bets={bets} />}
|
{!disabled && (
|
||||||
|
<ContractInfoDialog
|
||||||
|
contract={contract}
|
||||||
|
className={'hidden md:inline-flex'}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Row>
|
</Row>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function ExtraMobileContractDetails(props: {
|
||||||
|
contract: Contract
|
||||||
|
user: User | null | undefined
|
||||||
|
forceShowVolume?: boolean
|
||||||
|
}) {
|
||||||
|
const { contract, user, forceShowVolume } = props
|
||||||
|
const { volume, resolutionTime, closeTime, creatorId, uniqueBettorCount } =
|
||||||
|
contract
|
||||||
|
const uniqueBettors = uniqueBettorCount ?? 0
|
||||||
|
const { resolvedDate } = contractMetrics(contract)
|
||||||
|
const volumeTranslation =
|
||||||
|
volume > 800 || uniqueBettors > 20
|
||||||
|
? 'High'
|
||||||
|
: volume > 300 || uniqueBettors > 10
|
||||||
|
? 'Medium'
|
||||||
|
: 'Low'
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Row
|
||||||
|
className={clsx(
|
||||||
|
'items-center justify-around md:hidden',
|
||||||
|
user ? 'w-full' : ''
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{resolvedDate && resolutionTime ? (
|
||||||
|
<Col className={'items-center text-sm'}>
|
||||||
|
<Row className={'text-gray-500'}>
|
||||||
|
<DateTimeTooltip text="Market resolved:" time={resolutionTime}>
|
||||||
|
{resolvedDate}
|
||||||
|
</DateTimeTooltip>
|
||||||
|
</Row>
|
||||||
|
<Row className={'text-gray-400'}>Ended</Row>
|
||||||
|
</Col>
|
||||||
|
) : (
|
||||||
|
!resolvedDate &&
|
||||||
|
closeTime && (
|
||||||
|
<Col className={'items-center text-sm text-gray-500'}>
|
||||||
|
<EditableCloseDate
|
||||||
|
closeTime={closeTime}
|
||||||
|
contract={contract}
|
||||||
|
isCreator={creatorId === user?.id}
|
||||||
|
/>
|
||||||
|
<Row className={'text-gray-400'}>Ends</Row>
|
||||||
|
</Col>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
{(user || forceShowVolume) && (
|
||||||
|
<Col className={'items-center text-sm text-gray-500'}>
|
||||||
|
<Tooltip
|
||||||
|
text={`${formatMoney(
|
||||||
|
volume
|
||||||
|
)} bet - ${uniqueBettors} unique bettors`}
|
||||||
|
>
|
||||||
|
{volumeTranslation}
|
||||||
|
</Tooltip>
|
||||||
|
<Row className={'text-gray-400'}>Activity</Row>
|
||||||
|
</Col>
|
||||||
|
)}
|
||||||
|
</Row>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function EditableCloseDate(props: {
|
function EditableCloseDate(props: {
|
||||||
closeTime: number
|
closeTime: number
|
||||||
contract: Contract
|
contract: Contract
|
||||||
|
@ -318,10 +392,10 @@ function EditableCloseDate(props: {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{isEditingCloseTime ? (
|
{isEditingCloseTime ? (
|
||||||
<Row className="mr-1 items-start">
|
<Row className="z-10 mr-2 w-full shrink-0 items-start items-center gap-1">
|
||||||
<input
|
<input
|
||||||
type="date"
|
type="date"
|
||||||
className="input input-bordered"
|
className="input input-bordered shrink-0"
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
onChange={(e) => setCloseDate(e.target.value)}
|
onChange={(e) => setCloseDate(e.target.value)}
|
||||||
min={Date.now()}
|
min={Date.now()}
|
||||||
|
@ -329,39 +403,32 @@ function EditableCloseDate(props: {
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
type="time"
|
type="time"
|
||||||
className="input input-bordered ml-2"
|
className="input input-bordered shrink-0"
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
onChange={(e) => setCloseHoursMinutes(e.target.value)}
|
onChange={(e) => setCloseHoursMinutes(e.target.value)}
|
||||||
min="00:00"
|
min="00:00"
|
||||||
value={closeHoursMinutes}
|
value={closeHoursMinutes}
|
||||||
/>
|
/>
|
||||||
|
<Button size={'xs'} color={'blue'} onClick={onSave}>
|
||||||
|
Done
|
||||||
|
</Button>
|
||||||
</Row>
|
</Row>
|
||||||
) : (
|
) : (
|
||||||
<DateTimeTooltip
|
<DateTimeTooltip
|
||||||
text={closeTime > Date.now() ? 'Trading ends:' : 'Trading ended:'}
|
text={closeTime > Date.now() ? 'Trading ends:' : 'Trading ended:'}
|
||||||
time={closeTime}
|
time={closeTime}
|
||||||
>
|
>
|
||||||
{isSameYear
|
<span
|
||||||
? dayJsCloseTime.format('MMM D')
|
className={isCreator ? 'cursor-pointer' : ''}
|
||||||
: dayJsCloseTime.format('MMM D, YYYY')}
|
onClick={() => isCreator && setIsEditingCloseTime(true)}
|
||||||
{isSameDay && <> ({fromNow(closeTime)})</>}
|
>
|
||||||
|
{isSameYear
|
||||||
|
? dayJsCloseTime.format('MMM D')
|
||||||
|
: dayJsCloseTime.format('MMM D, YYYY')}
|
||||||
|
{isSameDay && <> ({fromNow(closeTime)})</>}
|
||||||
|
</span>
|
||||||
</DateTimeTooltip>
|
</DateTimeTooltip>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isCreator &&
|
|
||||||
(isEditingCloseTime ? (
|
|
||||||
<button className="btn btn-xs" onClick={onSave}>
|
|
||||||
Done
|
|
||||||
</button>
|
|
||||||
) : (
|
|
||||||
<Button
|
|
||||||
size={'xs'}
|
|
||||||
color={'gray-white'}
|
|
||||||
onClick={() => setIsEditingCloseTime(true)}
|
|
||||||
>
|
|
||||||
<PencilIcon className="!container mr-0.5 mb-0.5 inline h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
))}
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
import { DotsHorizontalIcon } from '@heroicons/react/outline'
|
import { DotsHorizontalIcon } from '@heroicons/react/outline'
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { uniqBy } from 'lodash'
|
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { Bet } from 'common/bet'
|
|
||||||
|
|
||||||
import { Contract } from 'common/contract'
|
import { Contract } from 'common/contract'
|
||||||
import { formatMoney } from 'common/util/format'
|
import { formatMoney } from 'common/util/format'
|
||||||
|
@ -22,8 +20,11 @@ import ShortToggle from '../widgets/short-toggle'
|
||||||
export const contractDetailsButtonClassName =
|
export const contractDetailsButtonClassName =
|
||||||
'group flex items-center rounded-md px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-100 text-gray-400 hover:text-gray-500'
|
'group flex items-center rounded-md px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-100 text-gray-400 hover:text-gray-500'
|
||||||
|
|
||||||
export function ContractInfoDialog(props: { contract: Contract; bets: Bet[] }) {
|
export function ContractInfoDialog(props: {
|
||||||
const { contract, bets } = props
|
contract: Contract
|
||||||
|
className?: string
|
||||||
|
}) {
|
||||||
|
const { contract, className } = props
|
||||||
|
|
||||||
const [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
const [featured, setFeatured] = useState(
|
const [featured, setFeatured] = useState(
|
||||||
|
@ -37,11 +38,7 @@ export function ContractInfoDialog(props: { contract: Contract; bets: Bet[] }) {
|
||||||
const { createdTime, closeTime, resolutionTime, mechanism, outcomeType, id } =
|
const { createdTime, closeTime, resolutionTime, mechanism, outcomeType, id } =
|
||||||
contract
|
contract
|
||||||
|
|
||||||
const tradersCount = uniqBy(
|
const bettorsCount = contract.uniqueBettorCount ?? 'Unknown'
|
||||||
bets.filter((bet) => !bet.isAnte),
|
|
||||||
'userId'
|
|
||||||
).length
|
|
||||||
|
|
||||||
const typeDisplay =
|
const typeDisplay =
|
||||||
outcomeType === 'BINARY'
|
outcomeType === 'BINARY'
|
||||||
? 'YES / NO'
|
? 'YES / NO'
|
||||||
|
@ -69,7 +66,7 @@ export function ContractInfoDialog(props: { contract: Contract; bets: Bet[] }) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<button
|
<button
|
||||||
className={contractDetailsButtonClassName}
|
className={clsx(contractDetailsButtonClassName, className)}
|
||||||
onClick={() => setOpen(true)}
|
onClick={() => setOpen(true)}
|
||||||
>
|
>
|
||||||
<DotsHorizontalIcon
|
<DotsHorizontalIcon
|
||||||
|
@ -136,8 +133,8 @@ export function ContractInfoDialog(props: { contract: Contract; bets: Bet[] }) {
|
||||||
</tr> */}
|
</tr> */}
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<td>Traders</td>
|
<td>Bettors</td>
|
||||||
<td>{tradersCount}</td>
|
<td>{bettorsCount}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
|
|
|
@ -18,10 +18,9 @@ import BetButton from '../bet-button'
|
||||||
import { AnswersGraph } from '../answers/answers-graph'
|
import { AnswersGraph } from '../answers/answers-graph'
|
||||||
import { Contract, CPMMBinaryContract } from 'common/contract'
|
import { Contract, CPMMBinaryContract } from 'common/contract'
|
||||||
import { ContractDescription } from './contract-description'
|
import { ContractDescription } from './contract-description'
|
||||||
import { ContractDetails } from './contract-details'
|
import { ContractDetails, ExtraMobileContractDetails } from './contract-details'
|
||||||
import { NumericGraph } from './numeric-graph'
|
import { NumericGraph } from './numeric-graph'
|
||||||
import { ShareRow } from './share-row'
|
import { ExtraContractActionsRow } from 'web/components/contract/extra-contract-actions-row'
|
||||||
import { LikeMarketButton } from 'web/components/contract/like-market-button'
|
|
||||||
|
|
||||||
export const ContractOverview = (props: {
|
export const ContractOverview = (props: {
|
||||||
contract: Contract
|
contract: Contract
|
||||||
|
@ -40,17 +39,15 @@ export const ContractOverview = (props: {
|
||||||
return (
|
return (
|
||||||
<Col className={clsx('mb-6', className)}>
|
<Col className={clsx('mb-6', className)}>
|
||||||
<Col className="gap-3 px-2 sm:gap-4">
|
<Col className="gap-3 px-2 sm:gap-4">
|
||||||
|
<ContractDetails
|
||||||
|
contract={contract}
|
||||||
|
user={user}
|
||||||
|
isCreator={isCreator}
|
||||||
|
/>
|
||||||
<Row className="justify-between gap-4">
|
<Row className="justify-between gap-4">
|
||||||
<div className="text-2xl text-indigo-700 md:text-3xl">
|
<div className="text-2xl text-indigo-700 md:text-3xl">
|
||||||
<Linkify text={question} />
|
<Linkify text={question} />
|
||||||
</div>
|
</div>
|
||||||
{(outcomeType === 'FREE_RESPONSE' ||
|
|
||||||
outcomeType === 'MULTIPLE_CHOICE') &&
|
|
||||||
!resolution && (
|
|
||||||
<div className={'sm:hidden'}>
|
|
||||||
<LikeMarketButton contract={contract} user={user} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<Row className={'hidden gap-3 xl:flex'}>
|
<Row className={'hidden gap-3 xl:flex'}>
|
||||||
{isBinary && (
|
{isBinary && (
|
||||||
<BinaryResolutionOrChance
|
<BinaryResolutionOrChance
|
||||||
|
@ -79,11 +76,9 @@ export const ContractOverview = (props: {
|
||||||
{isBinary ? (
|
{isBinary ? (
|
||||||
<Row className="items-center justify-between gap-4 xl:hidden">
|
<Row className="items-center justify-between gap-4 xl:hidden">
|
||||||
<BinaryResolutionOrChance contract={contract} />
|
<BinaryResolutionOrChance contract={contract} />
|
||||||
|
<ExtraMobileContractDetails contract={contract} user={user} />
|
||||||
{tradingAllowed(contract) && (
|
{tradingAllowed(contract) && (
|
||||||
<Row>
|
<Row>
|
||||||
<div className={'sm:hidden'}>
|
|
||||||
<LikeMarketButton contract={contract} user={user} />
|
|
||||||
</div>
|
|
||||||
<Col>
|
<Col>
|
||||||
<BetButton contract={contract as CPMMBinaryContract} />
|
<BetButton contract={contract as CPMMBinaryContract} />
|
||||||
{!user && (
|
{!user && (
|
||||||
|
@ -98,11 +93,9 @@ export const ContractOverview = (props: {
|
||||||
) : isPseudoNumeric ? (
|
) : isPseudoNumeric ? (
|
||||||
<Row className="items-center justify-between gap-4 xl:hidden">
|
<Row className="items-center justify-between gap-4 xl:hidden">
|
||||||
<PseudoNumericResolutionOrExpectation contract={contract} />
|
<PseudoNumericResolutionOrExpectation contract={contract} />
|
||||||
|
<ExtraMobileContractDetails contract={contract} user={user} />
|
||||||
{tradingAllowed(contract) && (
|
{tradingAllowed(contract) && (
|
||||||
<Row>
|
<Row>
|
||||||
<div className={'sm:hidden'}>
|
|
||||||
<LikeMarketButton contract={contract} user={user} />
|
|
||||||
</div>
|
|
||||||
<Col>
|
<Col>
|
||||||
<BetButton contract={contract} />
|
<BetButton contract={contract} />
|
||||||
{!user && (
|
{!user && (
|
||||||
|
@ -130,13 +123,6 @@ export const ContractOverview = (props: {
|
||||||
<NumericResolutionOrExpectation contract={contract} />
|
<NumericResolutionOrExpectation contract={contract} />
|
||||||
</Row>
|
</Row>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<ContractDetails
|
|
||||||
contract={contract}
|
|
||||||
bets={bets}
|
|
||||||
isCreator={isCreator}
|
|
||||||
user={user}
|
|
||||||
/>
|
|
||||||
</Col>
|
</Col>
|
||||||
<div className={'my-1 md:my-2'}></div>
|
<div className={'my-1 md:my-2'}></div>
|
||||||
{(isBinary || isPseudoNumeric) && (
|
{(isBinary || isPseudoNumeric) && (
|
||||||
|
@ -144,10 +130,17 @@ export const ContractOverview = (props: {
|
||||||
)}{' '}
|
)}{' '}
|
||||||
{(outcomeType === 'FREE_RESPONSE' ||
|
{(outcomeType === 'FREE_RESPONSE' ||
|
||||||
outcomeType === 'MULTIPLE_CHOICE') && (
|
outcomeType === 'MULTIPLE_CHOICE') && (
|
||||||
<AnswersGraph contract={contract} bets={bets} />
|
<Col className={'mb-1 gap-y-2'}>
|
||||||
|
<AnswersGraph contract={contract} bets={bets} />
|
||||||
|
<ExtraMobileContractDetails
|
||||||
|
contract={contract}
|
||||||
|
user={user}
|
||||||
|
forceShowVolume={true}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
)}
|
)}
|
||||||
{outcomeType === 'NUMERIC' && <NumericGraph contract={contract} />}
|
{outcomeType === 'NUMERIC' && <NumericGraph contract={contract} />}
|
||||||
<ShareRow user={user} contract={contract} />
|
<ExtraContractActionsRow user={user} contract={contract} />
|
||||||
<ContractDescription
|
<ContractDescription
|
||||||
className="px-2"
|
className="px-2"
|
||||||
contract={contract}
|
contract={contract}
|
||||||
|
|
|
@ -3,31 +3,25 @@ import { ShareIcon } from '@heroicons/react/outline'
|
||||||
|
|
||||||
import { Row } from '../layout/row'
|
import { Row } from '../layout/row'
|
||||||
import { Contract } from 'web/lib/firebase/contracts'
|
import { Contract } from 'web/lib/firebase/contracts'
|
||||||
import { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
import { Button } from 'web/components/button'
|
import { Button } from 'web/components/button'
|
||||||
import { CreateChallengeModal } from '../challenges/create-challenge-modal'
|
|
||||||
import { User } from 'common/user'
|
import { User } from 'common/user'
|
||||||
import { CHALLENGES_ENABLED } from 'common/challenge'
|
|
||||||
import { ShareModal } from './share-modal'
|
import { ShareModal } from './share-modal'
|
||||||
import { withTracking } from 'web/lib/service/analytics'
|
|
||||||
import { FollowMarketButton } from 'web/components/follow-market-button'
|
import { FollowMarketButton } from 'web/components/follow-market-button'
|
||||||
import { LikeMarketButton } from 'web/components/contract/like-market-button'
|
import { LikeMarketButton } from 'web/components/contract/like-market-button'
|
||||||
|
import { ContractInfoDialog } from 'web/components/contract/contract-info-dialog'
|
||||||
|
import { Col } from 'web/components/layout/col'
|
||||||
|
|
||||||
export function ShareRow(props: {
|
export function ExtraContractActionsRow(props: {
|
||||||
contract: Contract
|
contract: Contract
|
||||||
user: User | undefined | null
|
user: User | undefined | null
|
||||||
}) {
|
}) {
|
||||||
const { user, contract } = props
|
const { user, contract } = props
|
||||||
const { outcomeType, resolution } = contract
|
|
||||||
|
|
||||||
const showChallenge =
|
|
||||||
user && outcomeType === 'BINARY' && !resolution && CHALLENGES_ENABLED
|
|
||||||
|
|
||||||
const [isOpen, setIsOpen] = useState(false)
|
|
||||||
const [isShareOpen, setShareOpen] = useState(false)
|
const [isShareOpen, setShareOpen] = useState(false)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Row className="mt-0.5 sm:mt-2">
|
<Row className={'mt-0.5 justify-around sm:mt-2 lg:justify-start'}>
|
||||||
<Button
|
<Button
|
||||||
size="lg"
|
size="lg"
|
||||||
color="gray-white"
|
color="gray-white"
|
||||||
|
@ -36,8 +30,14 @@ export function ShareRow(props: {
|
||||||
setShareOpen(true)
|
setShareOpen(true)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ShareIcon className={clsx('mr-2 h-[24px] w-5')} aria-hidden="true" />
|
<Col className={'items-center sm:flex-row'}>
|
||||||
Share
|
<ShareIcon
|
||||||
|
className={clsx('h-[24px] w-5 sm:mr-2')}
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
<span>Share</span>
|
||||||
|
</Col>
|
||||||
|
|
||||||
<ShareModal
|
<ShareModal
|
||||||
isOpen={isShareOpen}
|
isOpen={isShareOpen}
|
||||||
setOpen={setShareOpen}
|
setOpen={setShareOpen}
|
||||||
|
@ -46,28 +46,13 @@ export function ShareRow(props: {
|
||||||
/>
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{showChallenge && (
|
|
||||||
<Button
|
|
||||||
size="lg"
|
|
||||||
color="gray-white"
|
|
||||||
onClick={withTracking(
|
|
||||||
() => setIsOpen(true),
|
|
||||||
'click challenge button'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
⚔️ Challenge
|
|
||||||
<CreateChallengeModal
|
|
||||||
isOpen={isOpen}
|
|
||||||
setOpen={setIsOpen}
|
|
||||||
user={user}
|
|
||||||
contract={contract}
|
|
||||||
/>
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
<FollowMarketButton contract={contract} user={user} />
|
<FollowMarketButton contract={contract} user={user} />
|
||||||
<div className={'hidden sm:block'}>
|
{user?.id !== contract.creatorId && (
|
||||||
<LikeMarketButton contract={contract} user={user} />
|
<LikeMarketButton contract={contract} user={user} />
|
||||||
</div>
|
)}
|
||||||
|
<Col className={'justify-center md:hidden'}>
|
||||||
|
<ContractInfoDialog contract={contract} />
|
||||||
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
)
|
)
|
||||||
}
|
}
|
|
@ -6,10 +6,11 @@ import { User } from 'common/user'
|
||||||
import { useUserLikes } from 'web/hooks/use-likes'
|
import { useUserLikes } from 'web/hooks/use-likes'
|
||||||
import toast from 'react-hot-toast'
|
import toast from 'react-hot-toast'
|
||||||
import { formatMoney } from 'common/util/format'
|
import { formatMoney } from 'common/util/format'
|
||||||
import { likeContract, unLikeContract } from 'web/lib/firebase/likes'
|
import { likeContract } from 'web/lib/firebase/likes'
|
||||||
import { LIKE_TIP_AMOUNT } from 'common/like'
|
import { LIKE_TIP_AMOUNT } from 'common/like'
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
import { Row } from 'web/components/layout/row'
|
import { Col } from 'web/components/layout/col'
|
||||||
|
import { firebaseLogin } from 'web/lib/firebase/users'
|
||||||
|
|
||||||
export function LikeMarketButton(props: {
|
export function LikeMarketButton(props: {
|
||||||
contract: Contract
|
contract: Contract
|
||||||
|
@ -18,16 +19,12 @@ export function LikeMarketButton(props: {
|
||||||
const { contract, user } = props
|
const { contract, user } = props
|
||||||
|
|
||||||
const likes = useUserLikes(user?.id)
|
const likes = useUserLikes(user?.id)
|
||||||
const likedContractIds = likes
|
const userLikedContractIds = likes
|
||||||
?.filter((l) => l.type === 'contract')
|
?.filter((l) => l.type === 'contract')
|
||||||
.map((l) => l.id)
|
.map((l) => l.id)
|
||||||
if (!user) return <div />
|
|
||||||
|
|
||||||
const onLike = async () => {
|
const onLike = async () => {
|
||||||
if (likedContractIds?.includes(contract.id)) {
|
if (!user) return firebaseLogin()
|
||||||
await unLikeContract(user.id, contract.id)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
await likeContract(user, contract)
|
await likeContract(user, contract)
|
||||||
toast(`You tipped ${contract.creatorName} ${formatMoney(LIKE_TIP_AMOUNT)}!`)
|
toast(`You tipped ${contract.creatorName} ${formatMoney(LIKE_TIP_AMOUNT)}!`)
|
||||||
}
|
}
|
||||||
|
@ -39,18 +36,19 @@ export function LikeMarketButton(props: {
|
||||||
color={'gray-white'}
|
color={'gray-white'}
|
||||||
onClick={onLike}
|
onClick={onLike}
|
||||||
>
|
>
|
||||||
<Row className={'gap-0 sm:gap-2'}>
|
<Col className={'sm:flex-row sm:gap-x-2'}>
|
||||||
<HeartIcon
|
<HeartIcon
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'h-6 w-6',
|
'h-6 w-6',
|
||||||
likedContractIds?.includes(contract.id) ||
|
user &&
|
||||||
(!likes && contract.likedByUserIds?.includes(user.id))
|
(userLikedContractIds?.includes(contract.id) ||
|
||||||
|
(!likes && contract.likedByUserIds?.includes(user.id)))
|
||||||
? 'fill-red-500 text-red-500'
|
? 'fill-red-500 text-red-500'
|
||||||
: ''
|
: ''
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<span className={'hidden sm:block'}>Tip</span>
|
Tip
|
||||||
</Row>
|
</Col>
|
||||||
</Button>
|
</Button>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,12 +12,15 @@ import { TweetButton } from '../tweet-button'
|
||||||
import { DuplicateContractButton } from '../copy-contract-button'
|
import { DuplicateContractButton } from '../copy-contract-button'
|
||||||
import { Button } from '../button'
|
import { Button } from '../button'
|
||||||
import { copyToClipboard } from 'web/lib/util/copy'
|
import { copyToClipboard } from 'web/lib/util/copy'
|
||||||
import { track } from 'web/lib/service/analytics'
|
import { track, withTracking } from 'web/lib/service/analytics'
|
||||||
import { ENV_CONFIG } from 'common/envs/constants'
|
import { ENV_CONFIG } from 'common/envs/constants'
|
||||||
import { User } from 'common/user'
|
import { User } from 'common/user'
|
||||||
import { SiteLink } from '../site-link'
|
import { SiteLink } from '../site-link'
|
||||||
import { formatMoney } from 'common/util/format'
|
import { formatMoney } from 'common/util/format'
|
||||||
import { REFERRAL_AMOUNT } from 'common/economy'
|
import { REFERRAL_AMOUNT } from 'common/economy'
|
||||||
|
import { CreateChallengeModal } from 'web/components/challenges/create-challenge-modal'
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { CHALLENGES_ENABLED } from 'common/challenge'
|
||||||
|
|
||||||
export function ShareModal(props: {
|
export function ShareModal(props: {
|
||||||
contract: Contract
|
contract: Contract
|
||||||
|
@ -26,8 +29,13 @@ export function ShareModal(props: {
|
||||||
setOpen: (open: boolean) => void
|
setOpen: (open: boolean) => void
|
||||||
}) {
|
}) {
|
||||||
const { contract, user, isOpen, setOpen } = props
|
const { contract, user, isOpen, setOpen } = props
|
||||||
|
const { outcomeType, resolution } = contract
|
||||||
|
|
||||||
|
const [openCreateChallengeModal, setOpenCreateChallengeModal] =
|
||||||
|
useState(false)
|
||||||
const linkIcon = <LinkIcon className="mr-2 h-6 w-6" aria-hidden="true" />
|
const linkIcon = <LinkIcon className="mr-2 h-6 w-6" aria-hidden="true" />
|
||||||
|
const showChallenge =
|
||||||
|
user && outcomeType === 'BINARY' && !resolution && CHALLENGES_ENABLED
|
||||||
|
|
||||||
const shareUrl = `https://${ENV_CONFIG.domain}${contractPath(contract)}${
|
const shareUrl = `https://${ENV_CONFIG.domain}${contractPath(contract)}${
|
||||||
user?.username && contract.creatorUsername !== user?.username
|
user?.username && contract.creatorUsername !== user?.username
|
||||||
|
@ -46,7 +54,6 @@ export function ShareModal(props: {
|
||||||
</SiteLink>{' '}
|
</SiteLink>{' '}
|
||||||
if a new user signs up using the link!
|
if a new user signs up using the link!
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
size="2xl"
|
size="2xl"
|
||||||
color="gradient"
|
color="gradient"
|
||||||
|
@ -61,8 +68,31 @@ export function ShareModal(props: {
|
||||||
>
|
>
|
||||||
{linkIcon} Copy link
|
{linkIcon} Copy link
|
||||||
</Button>
|
</Button>
|
||||||
|
{showChallenge && (
|
||||||
<Row className="z-0 justify-start gap-4 self-center">
|
<Button
|
||||||
|
size="lg"
|
||||||
|
color="gray-white"
|
||||||
|
className={'mb-2 flex max-w-xs self-center'}
|
||||||
|
onClick={withTracking(
|
||||||
|
() => setOpenCreateChallengeModal(true),
|
||||||
|
'click challenge button'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span>⚔️ Challenge a friend</span>
|
||||||
|
<CreateChallengeModal
|
||||||
|
isOpen={openCreateChallengeModal}
|
||||||
|
setOpen={(open) => {
|
||||||
|
if (!open) {
|
||||||
|
setOpenCreateChallengeModal(false)
|
||||||
|
setOpen(false)
|
||||||
|
} else setOpenCreateChallengeModal(open)
|
||||||
|
}}
|
||||||
|
user={user}
|
||||||
|
contract={contract}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Row className="z-0 flex-wrap justify-center gap-4 self-center">
|
||||||
<TweetButton
|
<TweetButton
|
||||||
className="self-start"
|
className="self-start"
|
||||||
tweetText={getTweetText(contract, shareUrl)}
|
tweetText={getTweetText(contract, shareUrl)}
|
||||||
|
|
|
@ -13,7 +13,7 @@ import { firebaseLogin, updateUser } from 'web/lib/firebase/users'
|
||||||
import { track } from 'web/lib/service/analytics'
|
import { track } from 'web/lib/service/analytics'
|
||||||
import { FollowMarketModal } from 'web/components/contract/follow-market-modal'
|
import { FollowMarketModal } from 'web/components/contract/follow-market-modal'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { Row } from 'web/components/layout/row'
|
import { Col } from 'web/components/layout/col'
|
||||||
|
|
||||||
export const FollowMarketButton = (props: {
|
export const FollowMarketButton = (props: {
|
||||||
contract: Contract
|
contract: Contract
|
||||||
|
@ -55,15 +55,15 @@ export const FollowMarketButton = (props: {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{followers?.includes(user?.id ?? 'nope') ? (
|
{followers?.includes(user?.id ?? 'nope') ? (
|
||||||
<Row className={'gap-2'}>
|
<Col className={'items-center gap-x-2 sm:flex-row'}>
|
||||||
<EyeOffIcon className={clsx('h-6 w-6')} aria-hidden="true" />
|
<EyeOffIcon className={clsx('h-6 w-6')} aria-hidden="true" />
|
||||||
Unwatch
|
Unwatch
|
||||||
</Row>
|
</Col>
|
||||||
) : (
|
) : (
|
||||||
<Row className={'gap-2'}>
|
<Col className={'items-center gap-x-2 sm:flex-row'}>
|
||||||
<EyeIcon className={clsx('h-6 w-6')} aria-hidden="true" />
|
<EyeIcon className={clsx('h-6 w-6')} aria-hidden="true" />
|
||||||
Watch
|
Watch
|
||||||
</Row>
|
</Col>
|
||||||
)}
|
)}
|
||||||
<FollowMarketModal
|
<FollowMarketModal
|
||||||
open={open}
|
open={open}
|
||||||
|
|
|
@ -11,7 +11,7 @@ function shortenName(name: string) {
|
||||||
const firstName = name.split(' ')[0]
|
const firstName = name.split(' ')[0]
|
||||||
const maxLength = 10
|
const maxLength = 10
|
||||||
const shortName =
|
const shortName =
|
||||||
firstName.length >= 3
|
firstName.length >= 4
|
||||||
? firstName.length < maxLength
|
? firstName.length < maxLength
|
||||||
? firstName
|
? firstName
|
||||||
: firstName.substring(0, maxLength - 3) + '...'
|
: firstName.substring(0, maxLength - 3) + '...'
|
||||||
|
|
|
@ -105,13 +105,7 @@ export function ContractEmbed(props: { contract: Contract; bets: Bet[] }) {
|
||||||
<Spacer h={3} />
|
<Spacer h={3} />
|
||||||
|
|
||||||
<Row className="items-center justify-between gap-4 px-2">
|
<Row className="items-center justify-between gap-4 px-2">
|
||||||
<ContractDetails
|
<ContractDetails contract={contract} user={null} disabled />
|
||||||
contract={contract}
|
|
||||||
bets={bets}
|
|
||||||
isCreator={false}
|
|
||||||
user={null}
|
|
||||||
disabled
|
|
||||||
/>
|
|
||||||
|
|
||||||
{(isBinary || isPseudoNumeric) &&
|
{(isBinary || isPseudoNumeric) &&
|
||||||
tradingAllowed(contract) &&
|
tradingAllowed(contract) &&
|
||||||
|
|
Loading…
Reference in New Issue
Block a user