manifold/web/components/amount-input.tsx

264 lines
6.8 KiB
TypeScript
Raw Normal View History

2022-01-11 03:41:42 +00:00
import clsx from 'clsx'
import _ from 'lodash'
2022-01-11 03:41:42 +00:00
import { useUser } from '../hooks/use-user'
import { formatMoney, formatWithCommas } from '../../common/util/format'
2022-01-11 03:41:42 +00:00
import { Col } from './layout/col'
import { Row } from './layout/row'
import { Bet, MAX_LOAN_PER_CONTRACT } from '../../common/bet'
import { InfoTooltip } from './info-tooltip'
import { Spacer } from './layout/spacer'
import { calculateCpmmSale } from '../../common/calculate-cpmm'
import { Binary, CPMM, FullContract } from '../../common/contract'
2022-01-11 03:41:42 +00:00
export function AmountInput(props: {
amount: number | undefined
onChange: (newAmount: number | undefined) => void
error: string | undefined
label: string
2022-01-11 03:41:42 +00:00
disabled?: boolean
className?: string
inputClassName?: string
// Needed to focus the amount input
inputRef?: React.MutableRefObject<any>
children?: any
2022-01-11 03:41:42 +00:00
}) {
const {
amount,
onChange,
error,
label,
2022-01-11 03:41:42 +00:00
disabled,
className,
inputClassName,
inputRef,
children,
2022-01-11 03:41:42 +00:00
} = props
const onAmountChange = (str: string) => {
const amount = parseInt(str.replace(/\D/g, ''))
const isInvalid = !str || isNaN(amount)
onChange(isInvalid ? undefined : amount)
2022-01-11 03:41:42 +00:00
}
return (
<Col className={className}>
<label className="input-group">
<span className="bg-gray-200 text-sm">{label}</span>
2022-01-11 03:41:42 +00:00
<input
className={clsx(
'input input-bordered',
error && 'input-error',
inputClassName
)}
ref={inputRef}
type="text"
pattern="[0-9]*"
inputMode="numeric"
2022-01-11 03:41:42 +00:00
placeholder="0"
maxLength={6}
2022-01-11 03:41:42 +00:00
value={amount ?? ''}
disabled={disabled}
onChange={(e) => onAmountChange(e.target.value)}
/>
</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}
</div>
)}
{children}
</Col>
)
}
export function BuyAmountInput(props: {
amount: number | undefined
onChange: (newAmount: number | undefined) => void
error: string | undefined
setError: (error: string | undefined) => void
contractIdForLoan: string | undefined
userBets?: Bet[]
minimumAmount?: number
disabled?: boolean
className?: string
inputClassName?: string
// Needed to focus the amount input
inputRef?: React.MutableRefObject<any>
}) {
const {
amount,
onChange,
userBets,
error,
setError,
contractIdForLoan,
disabled,
className,
inputClassName,
minimumAmount,
inputRef,
} = props
const user = useUser()
const openUserBets = (userBets ?? []).filter(
(bet) => !bet.isSold && !bet.sale
)
const prevLoanAmount = _.sumBy(openUserBets, (bet) => bet.loanAmount ?? 0)
const loanAmount = contractIdForLoan
? Math.min(amount ?? 0, MAX_LOAN_PER_CONTRACT - prevLoanAmount)
: 0
const onAmountChange = (amount: number | undefined) => {
onChange(amount)
// Check for errors.
if (amount !== undefined) {
const amountNetLoan = amount - loanAmount
if (user && user.balance < amountNetLoan) {
setError('Insufficient balance')
} else if (minimumAmount && amount < minimumAmount) {
setError('Minimum amount: ' + formatMoney(minimumAmount))
} else {
setError(undefined)
}
}
}
return (
<AmountInput
amount={amount}
onChange={onAmountChange}
label="M$"
error={error}
disabled={disabled}
className={className}
inputClassName={inputClassName}
inputRef={inputRef}
>
{user && (
<Col className="gap-3 text-sm">
{contractIdForLoan && (
<Row className="items-center justify-between gap-2 text-gray-500">
<Row className="items-center gap-2">
Amount loaned{' '}
<InfoTooltip
text={`In every market, you get an interest-free loan on the first ${formatMoney(
MAX_LOAN_PER_CONTRACT
)}.`}
/>
</Row>
<span className="text-neutral">{formatMoney(loanAmount)}</span>{' '}
</Row>
)}
</Col>
)}
</AmountInput>
)
}
export function SellAmountInput(props: {
contract: FullContract<CPMM, Binary>
amount: number | undefined
onChange: (newAmount: number | undefined) => void
userBets: Bet[]
error: string | undefined
setError: (error: string | undefined) => void
disabled?: boolean
className?: string
inputClassName?: string
// Needed to focus the amount input
inputRef?: React.MutableRefObject<any>
}) {
const {
contract,
amount,
onChange,
userBets,
error,
setError,
disabled,
className,
inputClassName,
inputRef,
} = props
const user = useUser()
const openUserBets = userBets.filter((bet) => !bet.isSold && !bet.sale)
const [yesBets, noBets] = _.partition(
openUserBets,
(bet) => bet.outcome === 'YES'
)
const [yesShares, noShares] = [
_.sumBy(yesBets, (bet) => bet.shares),
_.sumBy(noBets, (bet) => bet.shares),
]
const sellOutcome = yesShares ? 'YES' : noShares ? 'NO' : undefined
const shares = yesShares || noShares
const prevLoanAmount = _.sumBy(openUserBets, (bet) => bet.loanAmount ?? 0)
const sharesSold = Math.min(amount ?? 0, yesShares || noShares)
const { saleValue } = calculateCpmmSale(
contract,
sharesSold,
sellOutcome as 'YES' | 'NO'
)
const loanRepaid = Math.min(prevLoanAmount, saleValue)
const onAmountChange = (amount: number | undefined) => {
onChange(amount)
// Check for errors.
if (amount !== undefined) {
if (amount > shares) {
setError(`Maximum ${formatWithCommas(Math.floor(shares))} shares`)
} else {
setError(undefined)
}
}
}
return (
<AmountInput
amount={amount}
onChange={onAmountChange}
label="Qty"
error={error}
disabled={disabled}
className={className}
inputClassName={inputClassName}
inputRef={inputRef}
>
{user && (
<Col className="gap-3 text-sm">
<Row className="items-center justify-between gap-2 text-gray-500">
Sale proceeds{' '}
<span className="text-neutral">{formatMoney(saleValue)}</span>
</Row>
{!!prevLoanAmount && (
<Row className="items-center justify-between gap-2 text-gray-500">
<Row className="items-center gap-2">
Loan repaid{' '}
<InfoTooltip
text={`Sold shares go toward paying off loans first.`}
/>
</Row>
<span className="text-neutral">{formatMoney(loanRepaid)}</span>{' '}
</Row>
)}
</Col>
)}
</AmountInput>
2022-01-11 03:41:42 +00:00
)
}