* Answer datatype and MULTI outcome type for Contract
* Create free answer contract
* Automatically sort Tailwind classes with Prettier (#45)
* Add Prettier Tailwind plugin
* Autoformat Tailwind classes with Prettier
* Allow for non-binary contracts in contract page and related components
* logo with white inside, transparent bg
* Create answer
* Some UI for showing answers
* Answer bet panel
* Convert rest of calcuate file to generic multi contracts
* Working betting with ante'd NONE answer
* Numbered answers. Layout & calculation tweaks
* Can bet. More layout tweaks!
* Resolve answer UI
* Resolve multi market
* Resolved market UI
* Fix feed and cards for multi contracts
* Sell bets. Various fixes
* Tweaks for trades page
* Always dev mode
* Create answer bet has isAnte: true
* Fix  card showing 0% for multi contracts
* Fix grouped bets feed for multi outcomes
* None option converted to none of the above label at bottom of list. Button to resolve none.
* Tweaks to no answers yet, resolve button layout
* Show ante bets on new answers in the feed
* Update placeholder text for description
* Consolidate firestore rules for subcollections
* Remove Contract and Bet type params. Use string type for outcomes.
* Increase char limit to 10k for answers. Preserve line breaks.
* Don't show resolve options after answer chosen
* Fix type error in script
* Remove NONE resolution option
* Change outcomeType to include 'MULTI' and 'FREE_RESPONSE'
* Show bet probability change and payout when creating answer
* User info change: also change answers
* Append answers to contract field 'answers'
* sort trades by resolved
* Don't include trailing !:,.; in links
* Stop flooring inputs into formatMoney
* Revert "Stop flooring inputs into formatMoney"
This reverts commit 2f7ab18429.
* Consistently floor user.balance
* Expand create panel on focus
From Richard Hanania's feedback
* welcome email: include link to manifold
* Fix home page in dev on branches that are not free-response
* Close emails (#50)
* script init for stephen dev
* market close emails
* order of operations
* template email
* sendMarketCloseEmail: handle unsubscribe
* remove debugging
* marketCloseEmails: every hour
* sendMarketCloseEmails: check undefined
* marketCloseEmails: "every hour" => "every 1 hours"
* Set up a read API using Vercel serverless functions (#49)
* Set up read API using Vercel serverless functions
Featuring:
/api/v0/markets
/api/v0/market/[contractId]
/api/v0/slug/[contractSlug]
* Include tags in API
* Tweaks. Remove filter for only binary contract
* Fix bet probability change for NO bets
* Put back isProd calculation
Co-authored-by: Austin Chen <akrolsmir@gmail.com>
Co-authored-by: mantikoros <sgrugett@gmail.com>
Co-authored-by: mantikoros <95266179+mantikoros@users.noreply.github.com>
		
	
			
		
			
				
	
	
		
			226 lines
		
	
	
		
			6.2 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			226 lines
		
	
	
		
			6.2 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| import clsx from 'clsx'
 | |
| import React, { useEffect, useRef, useState } from 'react'
 | |
| 
 | |
| import { useUser } from '../hooks/use-user'
 | |
| import { Contract } from '../../common/contract'
 | |
| import { Col } from './layout/col'
 | |
| import { Row } from './layout/row'
 | |
| import { Spacer } from './layout/spacer'
 | |
| import { YesNoSelector } from './yes-no-selector'
 | |
| import {
 | |
|   formatMoney,
 | |
|   formatPercent,
 | |
|   formatWithCommas,
 | |
| } from '../../common/util/format'
 | |
| import { Title } from './title'
 | |
| import {
 | |
|   getProbability,
 | |
|   calculateShares,
 | |
|   getProbabilityAfterBet,
 | |
|   calculatePayoutAfterCorrectBet,
 | |
| } from '../../common/calculate'
 | |
| import { firebaseLogin } from '../lib/firebase/users'
 | |
| import { Bet } from '../../common/bet'
 | |
| import { placeBet } from '../lib/firebase/api-call'
 | |
| import { AmountInput } from './amount-input'
 | |
| import { InfoTooltip } from './info-tooltip'
 | |
| import { OutcomeLabel } from './outcome-label'
 | |
| 
 | |
| // Focus helper from https://stackoverflow.com/a/54159564/1222351
 | |
| function useFocus(): [React.RefObject<HTMLElement>, () => void] {
 | |
|   const htmlElRef = useRef<HTMLElement>(null)
 | |
|   const setFocus = () => {
 | |
|     htmlElRef.current && htmlElRef.current.focus()
 | |
|   }
 | |
| 
 | |
|   return [htmlElRef, setFocus]
 | |
| }
 | |
| 
 | |
| export function BetPanel(props: {
 | |
|   contract: Contract
 | |
|   className?: string
 | |
|   title?: string // Set if BetPanel is on a feed modal
 | |
|   selected?: 'YES' | 'NO'
 | |
|   onBetSuccess?: () => void
 | |
| }) {
 | |
|   useEffect(() => {
 | |
|     // warm up cloud function
 | |
|     placeBet({}).catch()
 | |
|   }, [])
 | |
| 
 | |
|   const { contract, className, title, selected, onBetSuccess } = props
 | |
|   const { totalShares, phantomShares } = contract
 | |
| 
 | |
|   const user = useUser()
 | |
| 
 | |
|   const [betChoice, setBetChoice] = useState<'YES' | 'NO' | undefined>(selected)
 | |
|   const [betAmount, setBetAmount] = useState<number | undefined>(undefined)
 | |
|   const [inputRef, focusAmountInput] = useFocus()
 | |
| 
 | |
|   const [error, setError] = useState<string | undefined>()
 | |
|   const [isSubmitting, setIsSubmitting] = useState(false)
 | |
|   const [wasSubmitted, setWasSubmitted] = useState(false)
 | |
| 
 | |
|   function onBetChoice(choice: 'YES' | 'NO') {
 | |
|     setBetChoice(choice)
 | |
|     setWasSubmitted(false)
 | |
|     focusAmountInput()
 | |
|   }
 | |
| 
 | |
|   function onBetChange(newAmount: number | undefined) {
 | |
|     setWasSubmitted(false)
 | |
|     setBetAmount(newAmount)
 | |
|     if (!betChoice) {
 | |
|       setBetChoice('YES')
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   async function submitBet() {
 | |
|     if (!user || !betAmount) return
 | |
| 
 | |
|     if (user.balance < betAmount) {
 | |
|       setError('Insufficient balance')
 | |
|       return
 | |
|     }
 | |
| 
 | |
|     setError(undefined)
 | |
|     setIsSubmitting(true)
 | |
| 
 | |
|     const result = await placeBet({
 | |
|       amount: betAmount,
 | |
|       outcome: betChoice,
 | |
|       contractId: contract.id,
 | |
|     }).then((r) => r.data as any)
 | |
| 
 | |
|     console.log('placed bet. Result:', result)
 | |
| 
 | |
|     if (result?.status === 'success') {
 | |
|       setIsSubmitting(false)
 | |
|       setWasSubmitted(true)
 | |
|       setBetAmount(undefined)
 | |
|       if (onBetSuccess) onBetSuccess()
 | |
|     } else {
 | |
|       setError(result?.error || 'Error placing bet')
 | |
|       setIsSubmitting(false)
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   const betDisabled = isSubmitting || !betAmount || error
 | |
| 
 | |
|   const initialProb = getProbability(contract.totalShares)
 | |
| 
 | |
|   const outcomeProb = getProbabilityAfterBet(
 | |
|     contract.totalShares,
 | |
|     betChoice || 'YES',
 | |
|     betAmount ?? 0
 | |
|   )
 | |
|   const resultProb = betChoice === 'NO' ? 1 - outcomeProb : outcomeProb
 | |
| 
 | |
|   const shares = calculateShares(
 | |
|     contract.totalShares,
 | |
|     betAmount ?? 0,
 | |
|     betChoice || 'YES'
 | |
|   )
 | |
| 
 | |
|   const currentPayout = betAmount
 | |
|     ? calculatePayoutAfterCorrectBet(contract, {
 | |
|         outcome: betChoice,
 | |
|         amount: betAmount,
 | |
|         shares,
 | |
|       } as Bet)
 | |
|     : 0
 | |
| 
 | |
|   const currentReturn = betAmount ? (currentPayout - betAmount) / betAmount : 0
 | |
|   const currentReturnPercent = (currentReturn * 100).toFixed() + '%'
 | |
|   const panelTitle = title ?? 'Place a trade'
 | |
|   if (title) {
 | |
|     focusAmountInput()
 | |
|   }
 | |
| 
 | |
|   return (
 | |
|     <Col className={clsx('rounded-md bg-white px-8 py-6', className)}>
 | |
|       <Title
 | |
|         className={clsx('!mt-0', title ? '!text-xl' : '')}
 | |
|         text={panelTitle}
 | |
|       />
 | |
| 
 | |
|       {/* <div className="mt-2 mb-1 text-sm text-gray-500">Outcome</div> */}
 | |
|       <YesNoSelector
 | |
|         className="mb-4"
 | |
|         selected={betChoice}
 | |
|         onSelect={(choice) => onBetChoice(choice)}
 | |
|       />
 | |
| 
 | |
|       <div className="my-3 text-left text-sm text-gray-500">Amount </div>
 | |
|       <AmountInput
 | |
|         inputClassName="w-full"
 | |
|         amount={betAmount}
 | |
|         onChange={onBetChange}
 | |
|         error={error}
 | |
|         setError={setError}
 | |
|         disabled={isSubmitting}
 | |
|         inputRef={inputRef}
 | |
|       />
 | |
| 
 | |
|       <Spacer h={4} />
 | |
| 
 | |
|       <div className="mt-2 mb-1 text-sm text-gray-500">Implied probability</div>
 | |
|       <Row>
 | |
|         <div>{formatPercent(initialProb)}</div>
 | |
|         <div className="mx-2">→</div>
 | |
|         <div>{formatPercent(resultProb)}</div>
 | |
|       </Row>
 | |
| 
 | |
|       {betChoice && (
 | |
|         <>
 | |
|           <Spacer h={4} />
 | |
|           <Row className="mt-2 mb-1 items-center gap-2 text-sm text-gray-500">
 | |
|             Payout if <OutcomeLabel outcome={betChoice} />
 | |
|             <InfoTooltip
 | |
|               text={`Current payout for ${formatWithCommas(
 | |
|                 shares
 | |
|               )} / ${formatWithCommas(
 | |
|                 shares +
 | |
|                   totalShares[betChoice] -
 | |
|                   (phantomShares ? phantomShares[betChoice] : 0)
 | |
|               )} ${betChoice} shares`}
 | |
|             />
 | |
|           </Row>
 | |
|           <div>
 | |
|             {formatMoney(currentPayout)}
 | |
|               <span>(+{currentReturnPercent})</span>
 | |
|           </div>
 | |
|         </>
 | |
|       )}
 | |
| 
 | |
|       <Spacer h={6} />
 | |
| 
 | |
|       {user ? (
 | |
|         <button
 | |
|           className={clsx(
 | |
|             'btn',
 | |
|             betDisabled
 | |
|               ? 'btn-disabled'
 | |
|               : betChoice === 'YES'
 | |
|               ? 'btn-primary'
 | |
|               : 'border-none bg-red-400 hover:bg-red-500',
 | |
|             isSubmitting ? 'loading' : ''
 | |
|           )}
 | |
|           onClick={betDisabled ? undefined : submitBet}
 | |
|         >
 | |
|           {isSubmitting ? 'Submitting...' : 'Submit trade'}
 | |
|         </button>
 | |
|       ) : (
 | |
|         <button
 | |
|           className="btn mt-4 whitespace-nowrap border-none bg-gradient-to-r from-teal-500 to-green-500 px-10 text-lg font-medium normal-case hover:from-teal-600 hover:to-green-600"
 | |
|           onClick={firebaseLogin}
 | |
|         >
 | |
|           Sign in to trade!
 | |
|         </button>
 | |
|       )}
 | |
| 
 | |
|       {wasSubmitted && <div className="mt-4">Trade submitted!</div>}
 | |
|     </Col>
 | |
|   )
 | |
| }
 |