Merge branch 'main' into loans2
This commit is contained in:
commit
79a61f0e98
|
@ -38,6 +38,7 @@ export type notification_source_types =
|
|||
| 'user'
|
||||
| 'bonus'
|
||||
| 'challenge'
|
||||
| 'betting_streak_bonus'
|
||||
| 'loan'
|
||||
|
||||
export type notification_source_update_types =
|
||||
|
@ -67,4 +68,5 @@ export type notification_reason_types =
|
|||
| 'bet_fill'
|
||||
| 'user_joined_from_your_group_invite'
|
||||
| 'challenge_accepted'
|
||||
| 'betting_streak_incremented'
|
||||
| 'loan_income'
|
||||
|
|
|
@ -4,3 +4,5 @@ export const NUMERIC_FIXED_VAR = 0.005
|
|||
export const NUMERIC_GRAPH_COLOR = '#5fa5f9'
|
||||
export const NUMERIC_TEXT_COLOR = 'text-blue-500'
|
||||
export const UNIQUE_BETTOR_BONUS_AMOUNT = 10
|
||||
export const BETTING_STREAK_BONUS_AMOUNT = 5
|
||||
export const BETTING_STREAK_RESET_HOUR = 9
|
||||
|
|
|
@ -16,7 +16,13 @@ export type Txn<T extends AnyTxnType = AnyTxnType> = {
|
|||
amount: number
|
||||
token: 'M$' // | 'USD' | MarketOutcome
|
||||
|
||||
category: 'CHARITY' | 'MANALINK' | 'TIP' | 'REFERRAL' | 'UNIQUE_BETTOR_BONUS'
|
||||
category:
|
||||
| 'CHARITY'
|
||||
| 'MANALINK'
|
||||
| 'TIP'
|
||||
| 'REFERRAL'
|
||||
| 'UNIQUE_BETTOR_BONUS'
|
||||
| 'BETTING_STREAK_BONUS'
|
||||
|
||||
// Any extra data
|
||||
data?: { [key: string]: any }
|
||||
|
@ -57,7 +63,7 @@ type Referral = {
|
|||
type Bonus = {
|
||||
fromType: 'BANK'
|
||||
toType: 'USER'
|
||||
category: 'UNIQUE_BETTOR_BONUS'
|
||||
category: 'UNIQUE_BETTOR_BONUS' | 'BETTING_STREAK_BONUS'
|
||||
}
|
||||
|
||||
export type DonationTxn = Txn & Donation
|
||||
|
|
|
@ -41,6 +41,8 @@ export type User = {
|
|||
referredByGroupId?: string
|
||||
lastPingTime?: number
|
||||
shouldShowWelcome?: boolean
|
||||
lastBetTime?: number
|
||||
currentBettingStreak?: number
|
||||
}
|
||||
|
||||
export const STARTING_BALANCE = ENV_CONFIG.startingBalance ?? 1000
|
||||
|
|
|
@ -524,3 +524,38 @@ export const createChallengeAcceptedNotification = async (
|
|||
}
|
||||
return await notificationRef.set(removeUndefinedProps(notification))
|
||||
}
|
||||
|
||||
export const createBettingStreakBonusNotification = async (
|
||||
user: User,
|
||||
txnId: string,
|
||||
bet: Bet,
|
||||
contract: Contract,
|
||||
amount: number,
|
||||
idempotencyKey: string
|
||||
) => {
|
||||
const notificationRef = firestore
|
||||
.collection(`/users/${user.id}/notifications`)
|
||||
.doc(idempotencyKey)
|
||||
const notification: Notification = {
|
||||
id: idempotencyKey,
|
||||
userId: user.id,
|
||||
reason: 'betting_streak_incremented',
|
||||
createdTime: Date.now(),
|
||||
isSeen: false,
|
||||
sourceId: txnId,
|
||||
sourceType: 'betting_streak_bonus',
|
||||
sourceUpdateType: 'created',
|
||||
sourceUserName: user.name,
|
||||
sourceUserUsername: user.username,
|
||||
sourceUserAvatarUrl: user.avatarUrl,
|
||||
sourceText: amount.toString(),
|
||||
sourceSlug: `/${contract.creatorUsername}/${contract.slug}/bets/${bet.id}`,
|
||||
sourceTitle: 'Betting Streak Bonus',
|
||||
// Perhaps not necessary, but just in case
|
||||
sourceContractSlug: contract.slug,
|
||||
sourceContractId: contract.id,
|
||||
sourceContractTitle: contract.question,
|
||||
sourceContractCreatorUsername: contract.creatorUsername,
|
||||
}
|
||||
return await notificationRef.set(removeUndefinedProps(notification))
|
||||
}
|
||||
|
|
|
@ -26,6 +26,7 @@ export * from './on-create-comment-on-group'
|
|||
export * from './on-create-txn'
|
||||
export * from './on-delete-group'
|
||||
export * from './score-contracts'
|
||||
export * from './reset-betting-streaks'
|
||||
|
||||
// v2
|
||||
export * from './health'
|
||||
|
|
|
@ -3,15 +3,20 @@ import * as admin from 'firebase-admin'
|
|||
import { keyBy, uniq } from 'lodash'
|
||||
|
||||
import { Bet, LimitBet } from '../../common/bet'
|
||||
import { getContract, getUser, getValues, isProd, log } from './utils'
|
||||
import { getUser, getValues, isProd, log } from './utils'
|
||||
import {
|
||||
createBetFillNotification,
|
||||
createBettingStreakBonusNotification,
|
||||
createNotification,
|
||||
} from './create-notification'
|
||||
import { filterDefined } from '../../common/util/array'
|
||||
import { Contract } from '../../common/contract'
|
||||
import { runTxn, TxnData } from './transact'
|
||||
import { UNIQUE_BETTOR_BONUS_AMOUNT } from '../../common/numeric-constants'
|
||||
import {
|
||||
BETTING_STREAK_BONUS_AMOUNT,
|
||||
BETTING_STREAK_RESET_HOUR,
|
||||
UNIQUE_BETTOR_BONUS_AMOUNT,
|
||||
} from '../../common/numeric-constants'
|
||||
import {
|
||||
DEV_HOUSE_LIQUIDITY_PROVIDER_ID,
|
||||
HOUSE_LIQUIDITY_PROVIDER_ID,
|
||||
|
@ -38,37 +43,99 @@ export const onCreateBet = functions.firestore
|
|||
.doc(contractId)
|
||||
.update({ lastBetTime, lastUpdatedTime: Date.now() })
|
||||
|
||||
await notifyFills(bet, contractId, eventId)
|
||||
await updateUniqueBettorsAndGiveCreatorBonus(
|
||||
contractId,
|
||||
eventId,
|
||||
bet.userId
|
||||
)
|
||||
const userContractSnap = await firestore
|
||||
.collection(`contracts`)
|
||||
.doc(contractId)
|
||||
.get()
|
||||
const contract = userContractSnap.data() as Contract
|
||||
|
||||
if (!contract) {
|
||||
log(`Could not find contract ${contractId}`)
|
||||
return
|
||||
}
|
||||
await updateUniqueBettorsAndGiveCreatorBonus(contract, eventId, bet.userId)
|
||||
|
||||
const bettor = await getUser(bet.userId)
|
||||
if (!bettor) return
|
||||
|
||||
await notifyFills(bet, contract, eventId, bettor)
|
||||
await updateBettingStreak(bettor, bet, contract, eventId)
|
||||
|
||||
await firestore.collection('users').doc(bettor.id).update({ lastBetTime })
|
||||
})
|
||||
|
||||
const updateBettingStreak = async (
|
||||
user: User,
|
||||
bet: Bet,
|
||||
contract: Contract,
|
||||
eventId: string
|
||||
) => {
|
||||
const betStreakResetTime = getTodaysBettingStreakResetTime()
|
||||
const lastBetTime = user?.lastBetTime ?? 0
|
||||
|
||||
// If they've already bet after the reset time, or if we haven't hit the reset time yet
|
||||
if (lastBetTime > betStreakResetTime || bet.createdTime < betStreakResetTime)
|
||||
return
|
||||
|
||||
const newBettingStreak = (user?.currentBettingStreak ?? 0) + 1
|
||||
// Otherwise, add 1 to their betting streak
|
||||
await firestore.collection('users').doc(user.id).update({
|
||||
currentBettingStreak: newBettingStreak,
|
||||
})
|
||||
|
||||
// Send them the bonus times their streak
|
||||
const bonusAmount = Math.min(
|
||||
BETTING_STREAK_BONUS_AMOUNT * newBettingStreak,
|
||||
100
|
||||
)
|
||||
const fromUserId = isProd()
|
||||
? HOUSE_LIQUIDITY_PROVIDER_ID
|
||||
: DEV_HOUSE_LIQUIDITY_PROVIDER_ID
|
||||
const bonusTxnDetails = {
|
||||
currentBettingStreak: newBettingStreak,
|
||||
}
|
||||
const result = await firestore.runTransaction(async (trans) => {
|
||||
const bonusTxn: TxnData = {
|
||||
fromId: fromUserId,
|
||||
fromType: 'BANK',
|
||||
toId: user.id,
|
||||
toType: 'USER',
|
||||
amount: bonusAmount,
|
||||
token: 'M$',
|
||||
category: 'BETTING_STREAK_BONUS',
|
||||
description: JSON.stringify(bonusTxnDetails),
|
||||
}
|
||||
return await runTxn(trans, bonusTxn)
|
||||
})
|
||||
if (!result.txn) {
|
||||
log("betting streak bonus txn couldn't be made")
|
||||
return
|
||||
}
|
||||
|
||||
await createBettingStreakBonusNotification(
|
||||
user,
|
||||
result.txn.id,
|
||||
bet,
|
||||
contract,
|
||||
bonusAmount,
|
||||
eventId
|
||||
)
|
||||
}
|
||||
|
||||
const updateUniqueBettorsAndGiveCreatorBonus = async (
|
||||
contractId: string,
|
||||
contract: Contract,
|
||||
eventId: string,
|
||||
bettorId: string
|
||||
) => {
|
||||
const userContractSnap = await firestore
|
||||
.collection(`contracts`)
|
||||
.doc(contractId)
|
||||
.get()
|
||||
const contract = userContractSnap.data() as Contract
|
||||
if (!contract) {
|
||||
log(`Could not find contract ${contractId}`)
|
||||
return
|
||||
}
|
||||
let previousUniqueBettorIds = contract.uniqueBettorIds
|
||||
|
||||
if (!previousUniqueBettorIds) {
|
||||
const contractBets = (
|
||||
await firestore.collection(`contracts/${contractId}/bets`).get()
|
||||
await firestore.collection(`contracts/${contract.id}/bets`).get()
|
||||
).docs.map((doc) => doc.data() as Bet)
|
||||
|
||||
if (contractBets.length === 0) {
|
||||
log(`No bets for contract ${contractId}`)
|
||||
log(`No bets for contract ${contract.id}`)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -86,7 +153,7 @@ const updateUniqueBettorsAndGiveCreatorBonus = async (
|
|||
if (!contract.uniqueBettorIds || isNewUniqueBettor) {
|
||||
log(`Got ${previousUniqueBettorIds} unique bettors`)
|
||||
isNewUniqueBettor && log(`And a new unique bettor ${bettorId}`)
|
||||
await firestore.collection(`contracts`).doc(contractId).update({
|
||||
await firestore.collection(`contracts`).doc(contract.id).update({
|
||||
uniqueBettorIds: newUniqueBettorIds,
|
||||
uniqueBettorCount: newUniqueBettorIds.length,
|
||||
})
|
||||
|
@ -97,7 +164,7 @@ const updateUniqueBettorsAndGiveCreatorBonus = async (
|
|||
|
||||
// Create combined txn for all new unique bettors
|
||||
const bonusTxnDetails = {
|
||||
contractId: contractId,
|
||||
contractId: contract.id,
|
||||
uniqueBettorIds: newUniqueBettorIds,
|
||||
}
|
||||
const fromUserId = isProd()
|
||||
|
@ -140,14 +207,14 @@ const updateUniqueBettorsAndGiveCreatorBonus = async (
|
|||
}
|
||||
}
|
||||
|
||||
const notifyFills = async (bet: Bet, contractId: string, eventId: string) => {
|
||||
const notifyFills = async (
|
||||
bet: Bet,
|
||||
contract: Contract,
|
||||
eventId: string,
|
||||
user: User
|
||||
) => {
|
||||
if (!bet.fills) return
|
||||
|
||||
const user = await getUser(bet.userId)
|
||||
if (!user) return
|
||||
const contract = await getContract(contractId)
|
||||
if (!contract) return
|
||||
|
||||
const matchedFills = bet.fills.filter((fill) => fill.matchedBetId !== null)
|
||||
const matchedBets = (
|
||||
await Promise.all(
|
||||
|
@ -180,3 +247,7 @@ const notifyFills = async (bet: Bet, contractId: string, eventId: string) => {
|
|||
})
|
||||
)
|
||||
}
|
||||
|
||||
const getTodaysBettingStreakResetTime = () => {
|
||||
return new Date().setUTCHours(BETTING_STREAK_RESET_HOUR, 0, 0, 0)
|
||||
}
|
||||
|
|
38
functions/src/reset-betting-streaks.ts
Normal file
38
functions/src/reset-betting-streaks.ts
Normal file
|
@ -0,0 +1,38 @@
|
|||
// check every day if the user has created a bet since 4pm UTC, and if not, reset their streak
|
||||
|
||||
import * as functions from 'firebase-functions'
|
||||
import * as admin from 'firebase-admin'
|
||||
import { User } from '../../common/user'
|
||||
import { DAY_MS } from '../../common/util/time'
|
||||
import { BETTING_STREAK_RESET_HOUR } from '../../common/numeric-constants'
|
||||
const firestore = admin.firestore()
|
||||
|
||||
export const resetBettingStreaksForUsers = functions.pubsub
|
||||
.schedule(`0 ${BETTING_STREAK_RESET_HOUR} * * *`)
|
||||
.onRun(async () => {
|
||||
await resetBettingStreaksInternal()
|
||||
})
|
||||
|
||||
const resetBettingStreaksInternal = async () => {
|
||||
const usersSnap = await firestore.collection('users').get()
|
||||
|
||||
const users = usersSnap.docs.map((doc) => doc.data() as User)
|
||||
|
||||
for (const user of users) {
|
||||
await resetBettingStreakForUser(user)
|
||||
}
|
||||
}
|
||||
|
||||
const resetBettingStreakForUser = async (user: User) => {
|
||||
const betStreakResetTime = Date.now() - DAY_MS
|
||||
// if they made a bet within the last day, don't reset their streak
|
||||
if (
|
||||
(user.lastBetTime ?? 0 > betStreakResetTime) ||
|
||||
!user.currentBettingStreak ||
|
||||
user.currentBettingStreak === 0
|
||||
)
|
||||
return
|
||||
await firestore.collection('users').doc(user.id).update({
|
||||
currentBettingStreak: 0,
|
||||
})
|
||||
}
|
|
@ -3,7 +3,6 @@ import React from 'react'
|
|||
import { useUser } from 'web/hooks/use-user'
|
||||
import { formatMoney } from 'common/util/format'
|
||||
import { Col } from './layout/col'
|
||||
import { Spacer } from './layout/spacer'
|
||||
import { SiteLink } from './site-link'
|
||||
import { ENV_CONFIG } from 'common/envs/constants'
|
||||
|
||||
|
@ -37,7 +36,7 @@ export function AmountInput(props: {
|
|||
|
||||
return (
|
||||
<Col className={className}>
|
||||
<label className="input-group">
|
||||
<label className="input-group mb-4">
|
||||
<span className="bg-gray-200 text-sm">{label}</span>
|
||||
<input
|
||||
className={clsx(
|
||||
|
@ -57,8 +56,6 @@ export function AmountInput(props: {
|
|||
/>
|
||||
</label>
|
||||
|
||||
<Spacer h={4} />
|
||||
|
||||
{error && (
|
||||
<div className="mb-2 mr-auto self-center whitespace-nowrap text-xs font-medium tracking-wide text-red-500">
|
||||
{error === 'Insufficient balance' ? (
|
||||
|
@ -115,6 +112,8 @@ export function BuyAmountInput(props: {
|
|||
} else {
|
||||
setError(undefined)
|
||||
}
|
||||
} else {
|
||||
setError(undefined)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -9,8 +9,8 @@ import { useUserContractBets } from 'web/hooks/use-user-bets'
|
|||
import { useSaveBinaryShares } from './use-save-binary-shares'
|
||||
import { Col } from './layout/col'
|
||||
|
||||
// Inline version of a bet panel. Opens BetPanel in a new modal.
|
||||
export default function BetRow(props: {
|
||||
/** Button that opens BetPanel in a new modal */
|
||||
export default function BetButton(props: {
|
||||
contract: CPMMBinaryContract | PseudoNumericContract
|
||||
className?: string
|
||||
btnClassName?: string
|
127
web/components/bet-inline.tsx
Normal file
127
web/components/bet-inline.tsx
Normal file
|
@ -0,0 +1,127 @@
|
|||
import { track } from '@amplitude/analytics-browser'
|
||||
import clsx from 'clsx'
|
||||
import { CPMMBinaryContract, PseudoNumericContract } from 'common/contract'
|
||||
import { getBinaryCpmmBetInfo } from 'common/new-bet'
|
||||
import { APIError } from 'web/lib/firebase/api'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useMutation } from 'react-query'
|
||||
import { placeBet } from 'web/lib/firebase/api'
|
||||
import { BuyAmountInput } from './amount-input'
|
||||
import { Button } from './button'
|
||||
import { Row } from './layout/row'
|
||||
import { YesNoSelector } from './yes-no-selector'
|
||||
import { useUnfilledBets } from 'web/hooks/use-bets'
|
||||
import { useUser } from 'web/hooks/use-user'
|
||||
import { SignUpPrompt } from './sign-up-prompt'
|
||||
import { getCpmmProbability } from 'common/calculate-cpmm'
|
||||
import { Col } from './layout/col'
|
||||
import { XIcon } from '@heroicons/react/solid'
|
||||
import { formatMoney } from 'common/util/format'
|
||||
|
||||
// adapted from bet-panel.ts
|
||||
export function BetInline(props: {
|
||||
contract: CPMMBinaryContract | PseudoNumericContract
|
||||
className?: string
|
||||
setProbAfter: (probAfter: number) => void
|
||||
onClose: () => void
|
||||
}) {
|
||||
const { contract, className, setProbAfter, onClose } = props
|
||||
|
||||
const user = useUser()
|
||||
|
||||
const [outcome, setOutcome] = useState<'YES' | 'NO'>('YES')
|
||||
const [amount, setAmount] = useState<number>()
|
||||
const [error, setError] = useState<string>()
|
||||
|
||||
const isPseudoNumeric = contract.outcomeType === 'PSEUDO_NUMERIC'
|
||||
const unfilledBets = useUnfilledBets(contract.id) ?? []
|
||||
|
||||
const { newPool, newP } = getBinaryCpmmBetInfo(
|
||||
outcome ?? 'YES',
|
||||
amount ?? 0,
|
||||
contract,
|
||||
undefined,
|
||||
unfilledBets
|
||||
)
|
||||
const resultProb = getCpmmProbability(newPool, newP)
|
||||
useEffect(() => setProbAfter(resultProb), [setProbAfter, resultProb])
|
||||
|
||||
const submitBet = useMutation(
|
||||
() => placeBet({ outcome, amount, contractId: contract.id }),
|
||||
{
|
||||
onError: (e) =>
|
||||
setError(e instanceof APIError ? e.toString() : 'Error placing bet'),
|
||||
onSuccess: () => {
|
||||
track('bet', {
|
||||
location: 'embed',
|
||||
outcomeType: contract.outcomeType,
|
||||
slug: contract.slug,
|
||||
contractId: contract.id,
|
||||
amount,
|
||||
outcome,
|
||||
isLimitOrder: false,
|
||||
})
|
||||
setAmount(undefined)
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
// reset error / success state on user change
|
||||
useEffect(() => {
|
||||
amount && submitBet.reset()
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [outcome, amount])
|
||||
|
||||
const tooFewFunds = error === 'Insufficient balance'
|
||||
|
||||
const betDisabled = submitBet.isLoading || tooFewFunds || !amount
|
||||
|
||||
return (
|
||||
<Col className={clsx('items-center', className)}>
|
||||
<Row className="h-12 items-stretch gap-3 rounded bg-indigo-200 py-2 px-3">
|
||||
<div className="text-xl">Bet</div>
|
||||
<YesNoSelector
|
||||
className="space-x-0"
|
||||
btnClassName="rounded-none first:rounded-l-2xl last:rounded-r-2xl"
|
||||
selected={outcome}
|
||||
onSelect={setOutcome}
|
||||
isPseudoNumeric={isPseudoNumeric}
|
||||
/>
|
||||
<BuyAmountInput
|
||||
className="-mb-4"
|
||||
inputClassName={clsx(
|
||||
'input-sm w-20 !text-base',
|
||||
error && 'input-error'
|
||||
)}
|
||||
amount={amount}
|
||||
onChange={setAmount}
|
||||
error="" // handle error ourselves
|
||||
setError={setError}
|
||||
/>
|
||||
{user && (
|
||||
<Button
|
||||
color={({ YES: 'green', NO: 'red' } as const)[outcome]}
|
||||
size="xs"
|
||||
disabled={betDisabled}
|
||||
onClick={() => submitBet.mutate()}
|
||||
>
|
||||
{submitBet.isLoading
|
||||
? 'Submitting'
|
||||
: submitBet.isSuccess
|
||||
? 'Success!'
|
||||
: 'Submit'}
|
||||
</Button>
|
||||
)}
|
||||
<SignUpPrompt size="xs" />
|
||||
<button onClick={onClose}>
|
||||
<XIcon className="ml-1 h-6 w-6" />
|
||||
</button>
|
||||
</Row>
|
||||
{error && (
|
||||
<div className="text-error my-1 text-sm">
|
||||
{error} {tooFewFunds && `(${formatMoney(user?.balance ?? 0)})`}
|
||||
</div>
|
||||
)}
|
||||
</Col>
|
||||
)
|
||||
}
|
|
@ -1,20 +1,23 @@
|
|||
import { ReactNode } from 'react'
|
||||
import clsx from 'clsx'
|
||||
|
||||
export type SizeType = '2xs' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl'
|
||||
export type ColorType =
|
||||
| 'green'
|
||||
| 'red'
|
||||
| 'blue'
|
||||
| 'indigo'
|
||||
| 'yellow'
|
||||
| 'gray'
|
||||
| 'gradient'
|
||||
| 'gray-white'
|
||||
|
||||
export function Button(props: {
|
||||
className?: string
|
||||
onClick?: () => void
|
||||
children?: ReactNode
|
||||
size?: '2xs' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl'
|
||||
color?:
|
||||
| 'green'
|
||||
| 'red'
|
||||
| 'blue'
|
||||
| 'indigo'
|
||||
| 'yellow'
|
||||
| 'gray'
|
||||
| 'gradient'
|
||||
| 'gray-white'
|
||||
size?: SizeType
|
||||
color?: ColorType
|
||||
type?: 'button' | 'reset' | 'submit'
|
||||
disabled?: boolean
|
||||
}) {
|
||||
|
|
|
@ -185,11 +185,16 @@ export function BinaryResolutionOrChance(props: {
|
|||
contract: BinaryContract
|
||||
large?: boolean
|
||||
className?: string
|
||||
probAfter?: number // 0 to 1
|
||||
}) {
|
||||
const { contract, large, className } = props
|
||||
const { contract, large, className, probAfter } = props
|
||||
const { resolution } = contract
|
||||
const textColor = `text-${getColor(contract)}`
|
||||
|
||||
const before = getBinaryProbPercent(contract)
|
||||
const after = probAfter && formatPercent(probAfter)
|
||||
const probChanged = before !== after
|
||||
|
||||
return (
|
||||
<Col className={clsx(large ? 'text-4xl' : 'text-3xl', className)}>
|
||||
{resolution ? (
|
||||
|
@ -206,7 +211,14 @@ export function BinaryResolutionOrChance(props: {
|
|||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className={textColor}>{getBinaryProbPercent(contract)}</div>
|
||||
{probAfter && probChanged ? (
|
||||
<div>
|
||||
<span className="text-gray-500 line-through">{before}</span>
|
||||
<span className={textColor}>{after}</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className={textColor}>{before}</div>
|
||||
)}
|
||||
<div className={clsx(textColor, large ? 'text-xl' : 'text-base')}>
|
||||
chance
|
||||
</div>
|
||||
|
|
|
@ -15,7 +15,7 @@ import {
|
|||
PseudoNumericResolutionOrExpectation,
|
||||
} from './contract-card'
|
||||
import { Bet } from 'common/bet'
|
||||
import BetRow from '../bet-row'
|
||||
import BetButton from '../bet-button'
|
||||
import { AnswersGraph } from '../answers/answers-graph'
|
||||
import { Contract, CPMMBinaryContract } from 'common/contract'
|
||||
import { ContractDescription } from './contract-description'
|
||||
|
@ -73,18 +73,18 @@ export const ContractOverview = (props: {
|
|||
<BinaryResolutionOrChance contract={contract} />
|
||||
|
||||
{tradingAllowed(contract) && (
|
||||
<BetRow contract={contract as CPMMBinaryContract} />
|
||||
<BetButton contract={contract as CPMMBinaryContract} />
|
||||
)}
|
||||
</Row>
|
||||
) : isPseudoNumeric ? (
|
||||
<Row className="items-center justify-between gap-4 xl:hidden">
|
||||
<PseudoNumericResolutionOrExpectation contract={contract} />
|
||||
{tradingAllowed(contract) && <BetRow contract={contract} />}
|
||||
{tradingAllowed(contract) && <BetButton contract={contract} />}
|
||||
</Row>
|
||||
) : isPseudoNumeric ? (
|
||||
<Row className="items-center justify-between gap-4 xl:hidden">
|
||||
<PseudoNumericResolutionOrExpectation contract={contract} />
|
||||
{tradingAllowed(contract) && <BetRow contract={contract} />}
|
||||
{tradingAllowed(contract) && <BetButton contract={contract} />}
|
||||
</Row>
|
||||
) : (
|
||||
(outcomeType === 'FREE_RESPONSE' ||
|
||||
|
|
|
@ -19,7 +19,7 @@ import { BinaryResolutionOrChance } from '../contract/contract-card'
|
|||
import { SiteLink } from '../site-link'
|
||||
import { Col } from '../layout/col'
|
||||
import { UserLink } from '../user-page'
|
||||
import BetRow from '../bet-row'
|
||||
import BetButton from '../bet-button'
|
||||
import { Avatar } from '../avatar'
|
||||
import { ActivityItem } from './activity-items'
|
||||
import { useUser } from 'web/hooks/use-user'
|
||||
|
@ -76,7 +76,7 @@ export function FeedItems(props: {
|
|||
) : (
|
||||
outcomeType === 'BINARY' &&
|
||||
tradingAllowed(contract) && (
|
||||
<BetRow
|
||||
<BetButton
|
||||
contract={contract as CPMMBinaryContract}
|
||||
className={clsx('mb-2', betRowClassName)}
|
||||
/>
|
||||
|
|
32
web/components/profile/betting-streak-modal.tsx
Normal file
32
web/components/profile/betting-streak-modal.tsx
Normal file
|
@ -0,0 +1,32 @@
|
|||
import { Modal } from 'web/components/layout/modal'
|
||||
import { Col } from 'web/components/layout/col'
|
||||
|
||||
export function BettingStreakModal(props: {
|
||||
isOpen: boolean
|
||||
setOpen: (open: boolean) => void
|
||||
}) {
|
||||
const { isOpen, setOpen } = props
|
||||
|
||||
return (
|
||||
<Modal open={isOpen} setOpen={setOpen}>
|
||||
<Col className="items-center gap-4 rounded-md bg-white px-8 py-6">
|
||||
<span className={'text-8xl'}>🔥</span>
|
||||
<span>Betting streaks are here!</span>
|
||||
<Col className={'gap-2'}>
|
||||
<span className={'text-indigo-700'}>• What are they?</span>
|
||||
<span className={'ml-2'}>
|
||||
You get a reward for every consecutive day that you place a bet. The
|
||||
more days you bet in a row, the more you earn!
|
||||
</span>
|
||||
<span className={'text-indigo-700'}>
|
||||
• Where can I check my streak?
|
||||
</span>
|
||||
<span className={'ml-2'}>
|
||||
You can see your current streak on the top right of your profile
|
||||
page.
|
||||
</span>
|
||||
</Col>
|
||||
</Col>
|
||||
</Modal>
|
||||
)
|
||||
}
|
|
@ -2,17 +2,21 @@ import React from 'react'
|
|||
import { useUser } from 'web/hooks/use-user'
|
||||
import { firebaseLogin } from 'web/lib/firebase/users'
|
||||
import { withTracking } from 'web/lib/service/analytics'
|
||||
import { Button } from './button'
|
||||
import { Button, SizeType } from './button'
|
||||
|
||||
export function SignUpPrompt(props: { label?: string; className?: string }) {
|
||||
const { label, className } = props
|
||||
export function SignUpPrompt(props: {
|
||||
label?: string
|
||||
className?: string
|
||||
size?: SizeType
|
||||
}) {
|
||||
const { label, className, size = 'lg' } = props
|
||||
const user = useUser()
|
||||
|
||||
return user === null ? (
|
||||
<Button
|
||||
onClick={withTracking(firebaseLogin, 'sign up to bet')}
|
||||
className={className}
|
||||
size="lg"
|
||||
size={size}
|
||||
color="gradient"
|
||||
>
|
||||
{label ?? 'Sign up to bet!'}
|
||||
|
|
|
@ -28,6 +28,7 @@ import { ReferralsButton } from 'web/components/referrals-button'
|
|||
import { formatMoney } from 'common/util/format'
|
||||
import { ShareIconButton } from 'web/components/share-icon-button'
|
||||
import { ENV_CONFIG } from 'common/envs/constants'
|
||||
import { BettingStreakModal } from 'web/components/profile/betting-streak-modal'
|
||||
|
||||
export function UserLink(props: {
|
||||
name: string
|
||||
|
@ -65,10 +66,13 @@ export function UserPage(props: { user: User }) {
|
|||
const isCurrentUser = user.id === currentUser?.id
|
||||
const bannerUrl = user.bannerUrl ?? defaultBannerUrl(user.id)
|
||||
const [showConfetti, setShowConfetti] = useState(false)
|
||||
const [showBettingStreakModal, setShowBettingStreakModal] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const claimedMana = router.query['claimed-mana'] === 'yes'
|
||||
setShowConfetti(claimedMana)
|
||||
const showBettingStreak = router.query['show'] === 'betting-streak'
|
||||
setShowBettingStreakModal(showBettingStreak)
|
||||
}, [router])
|
||||
|
||||
const profit = user.profitCached.allTime
|
||||
|
@ -80,9 +84,14 @@ export function UserPage(props: { user: User }) {
|
|||
description={user.bio ?? ''}
|
||||
url={`/${user.username}`}
|
||||
/>
|
||||
{showConfetti && (
|
||||
<FullscreenConfetti recycle={false} numberOfPieces={300} />
|
||||
)}
|
||||
{showConfetti ||
|
||||
(showBettingStreakModal && (
|
||||
<FullscreenConfetti recycle={false} numberOfPieces={300} />
|
||||
))}
|
||||
<BettingStreakModal
|
||||
isOpen={showBettingStreakModal}
|
||||
setOpen={setShowBettingStreakModal}
|
||||
/>
|
||||
{/* Banner image up top, with an circle avatar overlaid */}
|
||||
<div
|
||||
className="h-32 w-full bg-cover bg-center sm:h-40"
|
||||
|
@ -114,22 +123,34 @@ export function UserPage(props: { user: User }) {
|
|||
|
||||
{/* Profile details: name, username, bio, and link to twitter/discord */}
|
||||
<Col className="mx-4 -mt-6">
|
||||
<Row className={'items-center gap-2'}>
|
||||
<span className="text-2xl font-bold">{user.name}</span>
|
||||
<span className="mt-1 text-gray-500">
|
||||
<span
|
||||
className={clsx(
|
||||
'text-md',
|
||||
profit >= 0 ? 'text-green-600' : 'text-red-400'
|
||||
)}
|
||||
>
|
||||
{formatMoney(profit)}
|
||||
</span>{' '}
|
||||
profit
|
||||
</span>
|
||||
<Row className={'justify-between'}>
|
||||
<Col>
|
||||
<span className="text-2xl font-bold">{user.name}</span>
|
||||
<span className="text-gray-500">@{user.username}</span>
|
||||
</Col>
|
||||
<Col className={'justify-center'}>
|
||||
<Row className={'gap-3'}>
|
||||
<Col className={'items-center text-gray-500'}>
|
||||
<span
|
||||
className={clsx(
|
||||
'text-md',
|
||||
profit >= 0 ? 'text-green-600' : 'text-red-400'
|
||||
)}
|
||||
>
|
||||
{formatMoney(profit)}
|
||||
</span>
|
||||
<span>profit</span>
|
||||
</Col>
|
||||
<Col
|
||||
className={'cursor-pointer items-center text-gray-500'}
|
||||
onClick={() => setShowBettingStreakModal(true)}
|
||||
>
|
||||
<span>🔥{user.currentBettingStreak ?? 0}</span>
|
||||
<span>streak</span>
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
</Row>
|
||||
<span className="text-gray-500">@{user.username}</span>
|
||||
|
||||
<Spacer h={4} />
|
||||
{user.bio && (
|
||||
<>
|
||||
|
|
|
@ -38,7 +38,7 @@ export function YesNoSelector(props: {
|
|||
'hover:bg-primary-focus border-primary hover:border-primary-focus hover:text-white',
|
||||
selected == 'YES'
|
||||
? 'bg-primary text-white'
|
||||
: 'text-primary bg-transparent',
|
||||
: 'text-primary bg-white',
|
||||
btnClassName
|
||||
)}
|
||||
onClick={() => onSelect('YES')}
|
||||
|
@ -55,7 +55,7 @@ export function YesNoSelector(props: {
|
|||
'border-red-400 hover:border-red-500 hover:bg-red-500 hover:text-white',
|
||||
selected == 'NO'
|
||||
? 'bg-red-400 text-white'
|
||||
: 'bg-transparent text-red-400',
|
||||
: 'bg-white text-red-400',
|
||||
btnClassName
|
||||
)}
|
||||
onClick={() => onSelect('NO')}
|
||||
|
|
|
@ -67,7 +67,7 @@ export function groupNotifications(notifications: Notification[]) {
|
|||
const notificationGroupsByDay = groupBy(notifications, (notification) =>
|
||||
new Date(notification.createdTime).toDateString()
|
||||
)
|
||||
const incomeSourceTypes = ['bonus', 'tip', 'loan']
|
||||
const incomeSourceTypes = ['bonus', 'tip', 'loan', 'betting_streak_bonus']
|
||||
|
||||
Object.keys(notificationGroupsByDay).forEach((day) => {
|
||||
const notificationsGroupedByDay = notificationGroupsByDay[day]
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
import { Bet } from 'common/bet'
|
||||
import { Contract, CPMMBinaryContract } from 'common/contract'
|
||||
import { Contract } from 'common/contract'
|
||||
import { DOMAIN } from 'common/envs/constants'
|
||||
import { useState } from 'react'
|
||||
import { AnswersGraph } from 'web/components/answers/answers-graph'
|
||||
import BetRow from 'web/components/bet-row'
|
||||
import { BetInline } from 'web/components/bet-inline'
|
||||
import { Button } from 'web/components/button'
|
||||
import {
|
||||
BinaryResolutionOrChance,
|
||||
FreeResponseResolutionOrChance,
|
||||
|
@ -19,7 +21,6 @@ import { SiteLink } from 'web/components/site-link'
|
|||
import { useContractWithPreload } from 'web/hooks/use-contract'
|
||||
import { useMeasureSize } from 'web/hooks/use-measure-size'
|
||||
import { fromPropz, usePropz } from 'web/hooks/use-propz'
|
||||
import { useWindowSize } from 'web/hooks/use-window-size'
|
||||
import { listAllBets } from 'web/lib/firebase/bets'
|
||||
import {
|
||||
contractPath,
|
||||
|
@ -88,18 +89,15 @@ export function ContractEmbed(props: { contract: Contract; bets: Bet[] }) {
|
|||
|
||||
const href = `https://${DOMAIN}${contractPath(contract)}`
|
||||
|
||||
const { height: windowHeight } = useWindowSize()
|
||||
const { setElem, height: topSectionHeight } = useMeasureSize()
|
||||
const paddingBottom = 8
|
||||
const { setElem, height: graphHeight } = useMeasureSize()
|
||||
|
||||
const graphHeight =
|
||||
windowHeight && topSectionHeight
|
||||
? windowHeight - topSectionHeight - paddingBottom
|
||||
: 0
|
||||
const [betPanelOpen, setBetPanelOpen] = useState(false)
|
||||
|
||||
const [probAfter, setProbAfter] = useState<number>()
|
||||
|
||||
return (
|
||||
<Col className="w-full flex-1 bg-white">
|
||||
<div className="relative flex flex-col pt-2" ref={setElem}>
|
||||
<Col className="h-[100vh] w-full bg-white">
|
||||
<div className="relative flex flex-col pt-2">
|
||||
<div className="px-3 text-xl text-indigo-700 md:text-2xl">
|
||||
<SiteLink href={href}>{question}</SiteLink>
|
||||
</div>
|
||||
|
@ -114,25 +112,24 @@ export function ContractEmbed(props: { contract: Contract; bets: Bet[] }) {
|
|||
disabled
|
||||
/>
|
||||
|
||||
{(isBinary || isPseudoNumeric) &&
|
||||
tradingAllowed(contract) &&
|
||||
!betPanelOpen && (
|
||||
<Button color="gradient" onClick={() => setBetPanelOpen(true)}>
|
||||
Bet
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{isBinary && (
|
||||
<Row className="items-center gap-4">
|
||||
{tradingAllowed(contract) && (
|
||||
<BetRow
|
||||
contract={contract as CPMMBinaryContract}
|
||||
betPanelClassName="scale-75"
|
||||
/>
|
||||
)}
|
||||
<BinaryResolutionOrChance contract={contract} />
|
||||
</Row>
|
||||
<BinaryResolutionOrChance
|
||||
contract={contract}
|
||||
probAfter={probAfter}
|
||||
className="items-center"
|
||||
/>
|
||||
)}
|
||||
|
||||
{isPseudoNumeric && (
|
||||
<Row className="items-center gap-4">
|
||||
{tradingAllowed(contract) && (
|
||||
<BetRow contract={contract} betPanelClassName="scale-75" />
|
||||
)}
|
||||
<PseudoNumericResolutionOrExpectation contract={contract} />
|
||||
</Row>
|
||||
<PseudoNumericResolutionOrExpectation contract={contract} />
|
||||
)}
|
||||
|
||||
{outcomeType === 'FREE_RESPONSE' && (
|
||||
|
@ -150,7 +147,16 @@ export function ContractEmbed(props: { contract: Contract; bets: Bet[] }) {
|
|||
<Spacer h={2} />
|
||||
</div>
|
||||
|
||||
<div className="mx-1" style={{ paddingBottom }}>
|
||||
{(isBinary || isPseudoNumeric) && betPanelOpen && (
|
||||
<BetInline
|
||||
contract={contract as any}
|
||||
setProbAfter={setProbAfter}
|
||||
onClose={() => setBetPanelOpen(false)}
|
||||
className="self-center"
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="mx-1 mb-2 min-h-0 flex-1" ref={setElem}>
|
||||
{(isBinary || isPseudoNumeric) && (
|
||||
<ContractProbGraph
|
||||
contract={contract}
|
||||
|
|
|
@ -31,7 +31,10 @@ import {
|
|||
import { TrendingUpIcon } from '@heroicons/react/outline'
|
||||
import { formatMoney } from 'common/util/format'
|
||||
import { groupPath } from 'web/lib/firebase/groups'
|
||||
import { UNIQUE_BETTOR_BONUS_AMOUNT } from 'common/numeric-constants'
|
||||
import {
|
||||
BETTING_STREAK_BONUS_AMOUNT,
|
||||
UNIQUE_BETTOR_BONUS_AMOUNT,
|
||||
} from 'common/numeric-constants'
|
||||
import { groupBy, sum, uniq } from 'lodash'
|
||||
import { track } from '@amplitude/analytics-browser'
|
||||
import { Pagination } from 'web/components/pagination'
|
||||
|
@ -229,39 +232,39 @@ function IncomeNotificationGroupItem(props: {
|
|||
(n) => n.sourceType
|
||||
)
|
||||
for (const sourceType in groupedNotificationsBySourceType) {
|
||||
// Source title splits by contracts and groups
|
||||
// Source title splits by contracts, groups, betting streak bonus
|
||||
const groupedNotificationsBySourceTitle = groupBy(
|
||||
groupedNotificationsBySourceType[sourceType],
|
||||
(notification) => {
|
||||
return notification.sourceTitle ?? notification.sourceContractTitle
|
||||
}
|
||||
)
|
||||
for (const contractId in groupedNotificationsBySourceTitle) {
|
||||
const notificationsForContractId =
|
||||
groupedNotificationsBySourceTitle[contractId]
|
||||
if (notificationsForContractId.length === 1) {
|
||||
newNotifications.push(notificationsForContractId[0])
|
||||
for (const sourceTitle in groupedNotificationsBySourceTitle) {
|
||||
const notificationsForSourceTitle =
|
||||
groupedNotificationsBySourceTitle[sourceTitle]
|
||||
if (notificationsForSourceTitle.length === 1) {
|
||||
newNotifications.push(notificationsForSourceTitle[0])
|
||||
continue
|
||||
}
|
||||
let sum = 0
|
||||
notificationsForContractId.forEach(
|
||||
notificationsForSourceTitle.forEach(
|
||||
(notification) =>
|
||||
notification.sourceText &&
|
||||
(sum = parseInt(notification.sourceText) + sum)
|
||||
)
|
||||
const uniqueUsers = uniq(
|
||||
notificationsForContractId.map((notification) => {
|
||||
notificationsForSourceTitle.map((notification) => {
|
||||
return notification.sourceUserUsername
|
||||
})
|
||||
)
|
||||
|
||||
const newNotification = {
|
||||
...notificationsForContractId[0],
|
||||
...notificationsForSourceTitle[0],
|
||||
sourceText: sum.toString(),
|
||||
sourceUserUsername:
|
||||
uniqueUsers.length > 1
|
||||
? MULTIPLE_USERS_KEY
|
||||
: notificationsForContractId[0].sourceType,
|
||||
: notificationsForSourceTitle[0].sourceType,
|
||||
}
|
||||
newNotifications.push(newNotification)
|
||||
}
|
||||
|
@ -362,7 +365,8 @@ function IncomeNotificationItem(props: {
|
|||
justSummary?: boolean
|
||||
}) {
|
||||
const { notification, justSummary } = props
|
||||
const { sourceType, sourceUserName, sourceUserUsername } = notification
|
||||
const { sourceType, sourceUserName, sourceUserUsername, sourceText } =
|
||||
notification
|
||||
const [highlighted] = useState(!notification.isSeen)
|
||||
const { width } = useWindowSize()
|
||||
const isMobile = (width && width < 768) || false
|
||||
|
@ -370,19 +374,74 @@ function IncomeNotificationItem(props: {
|
|||
setNotificationsAsSeen([notification])
|
||||
}, [notification])
|
||||
|
||||
function getReasonForShowingIncomeNotification(simple: boolean) {
|
||||
function reasonAndLink(simple: boolean) {
|
||||
const { sourceText } = notification
|
||||
let reasonText = ''
|
||||
if (sourceType === 'bonus' && sourceText) {
|
||||
reasonText = !simple
|
||||
? `Bonus for ${
|
||||
parseInt(sourceText) / UNIQUE_BETTOR_BONUS_AMOUNT
|
||||
} unique traders`
|
||||
} unique traders on`
|
||||
: 'bonus on'
|
||||
} else if (sourceType === 'tip') {
|
||||
reasonText = !simple ? `tipped you` : `in tips on`
|
||||
reasonText = !simple ? `tipped you on` : `in tips on`
|
||||
} else if (sourceType === 'betting_streak_bonus' && sourceText) {
|
||||
reasonText = `for your ${
|
||||
parseInt(sourceText) / BETTING_STREAK_BONUS_AMOUNT
|
||||
}-day`
|
||||
}
|
||||
return reasonText
|
||||
return (
|
||||
<>
|
||||
{reasonText}
|
||||
{sourceType === 'betting_streak_bonus' ? (
|
||||
simple ? (
|
||||
<span className={'ml-1 font-bold'}>Betting Streak</span>
|
||||
) : (
|
||||
<SiteLink
|
||||
className={'ml-1 font-bold'}
|
||||
href={'/betting-streak-bonus'}
|
||||
>
|
||||
Betting Streak
|
||||
</SiteLink>
|
||||
)
|
||||
) : (
|
||||
<QuestionOrGroupLink
|
||||
notification={notification}
|
||||
ignoreClick={isMobile}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const incomeNotificationLabel = () => {
|
||||
return sourceText ? (
|
||||
<span className="text-primary">
|
||||
{'+' + formatMoney(parseInt(sourceText))}
|
||||
</span>
|
||||
) : (
|
||||
<div />
|
||||
)
|
||||
}
|
||||
|
||||
const getIncomeSourceUrl = () => {
|
||||
const {
|
||||
sourceId,
|
||||
sourceContractCreatorUsername,
|
||||
sourceContractSlug,
|
||||
sourceSlug,
|
||||
} = notification
|
||||
if (sourceType === 'tip' && sourceContractSlug)
|
||||
return `/${sourceContractCreatorUsername}/${sourceContractSlug}#${sourceSlug}`
|
||||
if (sourceType === 'tip' && sourceSlug) return `${groupPath(sourceSlug)}`
|
||||
if (sourceType === 'challenge') return `${sourceSlug}`
|
||||
if (sourceType === 'betting_streak_bonus')
|
||||
return `/${sourceUserUsername}/?show=betting-streak`
|
||||
if (sourceContractCreatorUsername && sourceContractSlug)
|
||||
return `/${sourceContractCreatorUsername}/${sourceContractSlug}#${getSourceIdForLinkComponent(
|
||||
sourceId ?? '',
|
||||
sourceType
|
||||
)}`
|
||||
}
|
||||
|
||||
if (justSummary) {
|
||||
|
@ -392,19 +451,9 @@ function IncomeNotificationItem(props: {
|
|||
<div className={'flex pl-1 sm:pl-0'}>
|
||||
<div className={'inline-flex overflow-hidden text-ellipsis pl-1'}>
|
||||
<div className={'mr-1 text-black'}>
|
||||
<NotificationTextLabel
|
||||
className={'line-clamp-1'}
|
||||
notification={notification}
|
||||
justSummary={true}
|
||||
/>
|
||||
{incomeNotificationLabel()}
|
||||
</div>
|
||||
<span className={'flex truncate'}>
|
||||
{getReasonForShowingIncomeNotification(true)}
|
||||
<QuestionOrGroupLink
|
||||
notification={notification}
|
||||
ignoreClick={isMobile}
|
||||
/>
|
||||
</span>
|
||||
<span className={'flex truncate'}>{reasonAndLink(true)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -421,18 +470,16 @@ function IncomeNotificationItem(props: {
|
|||
>
|
||||
<div className={'relative'}>
|
||||
<SiteLink
|
||||
href={getSourceUrl(notification) ?? ''}
|
||||
href={getIncomeSourceUrl() ?? ''}
|
||||
className={'absolute left-0 right-0 top-0 bottom-0 z-0'}
|
||||
/>
|
||||
<Row className={'items-center text-gray-500 sm:justify-start'}>
|
||||
<div className={'line-clamp-2 flex max-w-xl shrink '}>
|
||||
<div className={'inline'}>
|
||||
<span className={'mr-1'}>
|
||||
<NotificationTextLabel notification={notification} />
|
||||
</span>
|
||||
<span className={'mr-1'}>{incomeNotificationLabel()}</span>
|
||||
</div>
|
||||
<span>
|
||||
{sourceType != 'bonus' &&
|
||||
{sourceType === 'tip' &&
|
||||
(sourceUserUsername === MULTIPLE_USERS_KEY ? (
|
||||
<span className={'mr-1 truncate'}>Multiple users</span>
|
||||
) : (
|
||||
|
@ -443,8 +490,7 @@ function IncomeNotificationItem(props: {
|
|||
short={true}
|
||||
/>
|
||||
))}
|
||||
{getReasonForShowingIncomeNotification(false)} {' on'}
|
||||
<QuestionOrGroupLink notification={notification} />
|
||||
{reasonAndLink(false)}
|
||||
</span>
|
||||
</div>
|
||||
</Row>
|
||||
|
@ -794,9 +840,6 @@ function getSourceUrl(notification: Notification) {
|
|||
// User referral:
|
||||
if (sourceType === 'user' && !sourceContractSlug)
|
||||
return `/${sourceUserUsername}`
|
||||
if (sourceType === 'tip' && sourceContractSlug)
|
||||
return `/${sourceContractCreatorUsername}/${sourceContractSlug}#${sourceSlug}`
|
||||
if (sourceType === 'tip' && sourceSlug) return `${groupPath(sourceSlug)}`
|
||||
if (sourceType === 'challenge') return `${sourceSlug}`
|
||||
if (sourceContractCreatorUsername && sourceContractSlug)
|
||||
return `/${sourceContractCreatorUsername}/${sourceContractSlug}#${getSourceIdForLinkComponent(
|
||||
|
@ -885,12 +928,6 @@ function NotificationTextLabel(props: {
|
|||
return (
|
||||
<span className="text-blue-400">{formatMoney(parseInt(sourceText))}</span>
|
||||
)
|
||||
} else if ((sourceType === 'bonus' || sourceType === 'tip') && sourceText) {
|
||||
return (
|
||||
<span className="text-primary">
|
||||
{'+' + formatMoney(parseInt(sourceText))}
|
||||
</span>
|
||||
)
|
||||
} else if (sourceType === 'bet' && sourceText) {
|
||||
return (
|
||||
<>
|
||||
|
|
Loading…
Reference in New Issue
Block a user