diff --git a/web/components/challenges/create-challenge-button.tsx b/web/components/challenges/create-challenge-button.tsx new file mode 100644 index 00000000..6eab9bc5 --- /dev/null +++ b/web/components/challenges/create-challenge-button.tsx @@ -0,0 +1,255 @@ +import clsx from 'clsx' +import dayjs from 'dayjs' +import React, { useEffect, useState } from 'react' +import { LinkIcon, SwitchVerticalIcon } from '@heroicons/react/outline' + +import { Col } from '../layout/col' +import { Row } from '../layout/row' +import { Title } from '../title' +import { User } from 'common/user' +import { Modal } from 'web/components/layout/modal' +import { Button } from '../button' +import { createChallenge, getChallengeUrl } from 'web/lib/firebase/challenges' +import { BinaryContract } from 'common/contract' +import { SiteLink } from 'web/components/site-link' +import { formatMoney } from 'common/util/format' +import { NoLabel, YesLabel } from '../outcome-label' +import { QRCode } from '../qr-code' +import { copyToClipboard } from 'web/lib/util/copy' +import toast from 'react-hot-toast' + +type challengeInfo = { + amount: number + expiresTime: number | null + message: string + outcome: 'YES' | 'NO' | number + acceptorAmount: number +} +export function CreateChallengeButton(props: { + user: User | null | undefined + contract: BinaryContract +}) { + const { user, contract } = props + const [open, setOpen] = useState(false) + const [challengeSlug, setChallengeSlug] = useState('') + + return ( + <> + setOpen(newOpen)} size={'sm'}> + + {/*// add a sign up to challenge button?*/} + {user && ( + { + const challenge = await createChallenge({ + creator: user, + creatorAmount: newChallenge.amount, + expiresTime: newChallenge.expiresTime, + message: newChallenge.message, + acceptorAmount: newChallenge.acceptorAmount, + outcome: newChallenge.outcome, + contract: contract, + }) + challenge && setChallengeSlug(getChallengeUrl(challenge)) + }} + challengeSlug={challengeSlug} + /> + )} + + + + + + ) +} + +function CreateChallengeForm(props: { + user: User + contract: BinaryContract + onCreate: (m: challengeInfo) => Promise + challengeSlug: string +}) { + const { user, onCreate, contract, challengeSlug } = props + const [isCreating, setIsCreating] = useState(false) + const [finishedCreating, setFinishedCreating] = useState(false) + const [error, setError] = useState('') + const [editingAcceptorAmount, setEditingAcceptorAmount] = useState(false) + const defaultExpire = 'week' + + const defaultMessage = `${user.name} is challenging you to a bet! Do you think ${contract.question}` + + const [challengeInfo, setChallengeInfo] = useState({ + expiresTime: dayjs().add(2, defaultExpire).valueOf(), + outcome: 'YES', + amount: 100, + acceptorAmount: 100, + message: defaultMessage, + }) + useEffect(() => { + setError('') + }, [challengeInfo]) + + return ( + <> + {!finishedCreating && ( +
{ + e.preventDefault() + if (user.balance < challengeInfo.amount) { + setError('You do not have enough mana to create this challenge') + return + } + setIsCreating(true) + onCreate(challengeInfo).finally(() => setIsCreating(false)) + setFinishedCreating(true) + }} + > + + <div className="mt-2 flex flex-col flex-wrap justify-center gap-x-5 gap-y-2"> + <div>You'll bet:</div> + <Row + className={ + 'form-control w-full max-w-xs items-center justify-between gap-4 pr-3' + } + > + <Col> + <div className="relative"> + <span className="absolute mx-3 mt-3.5 text-sm text-gray-400"> + M$ + </span> + <input + className="input input-bordered w-32 pl-10" + type="number" + min={1} + value={challengeInfo.amount} + onChange={(e) => + setChallengeInfo((m: challengeInfo) => { + return { + ...m, + amount: parseInt(e.target.value), + acceptorAmount: editingAcceptorAmount + ? m.acceptorAmount + : parseInt(e.target.value), + } + }) + } + /> + </div> + </Col> + <span className={''}>on</span> + {challengeInfo.outcome === 'YES' ? <YesLabel /> : <NoLabel />} + </Row> + <Row className={'mt-3 max-w-xs justify-end'}> + <Button + color={'gradient'} + className={'opacity-80'} + onClick={() => + setChallengeInfo((m: challengeInfo) => { + return { + ...m, + outcome: m.outcome === 'YES' ? 'NO' : 'YES', + } + }) + } + > + <SwitchVerticalIcon className={'h-4 w-4'} /> + </Button> + </Row> + <Row className={'items-center'}>If they bet:</Row> + <Row className={'max-w-xs items-center justify-between gap-4 pr-3'}> + <div className={'w-32 sm:mr-1'}> + {editingAcceptorAmount ? ( + <Col> + <div className="relative"> + <span className="absolute mx-3 mt-3.5 text-sm text-gray-400"> + M$ + </span> + <input + className="input input-bordered w-32 pl-10" + type="number" + min={1} + value={challengeInfo.acceptorAmount} + onChange={(e) => + setChallengeInfo((m: challengeInfo) => { + return { + ...m, + acceptorAmount: parseInt(e.target.value), + } + }) + } + /> + </div> + </Col> + ) : ( + <span className="ml-1 font-bold"> + {formatMoney(challengeInfo.acceptorAmount)} + </span> + )} + </div> + <span>on</span> + {challengeInfo.outcome === 'YES' ? <NoLabel /> : <YesLabel />} + </Row> + </div> + <Row + className={clsx( + 'mt-8', + !editingAcceptorAmount ? 'justify-between' : 'justify-end' + )} + > + {!editingAcceptorAmount && ( + <Button + color={'gray-white'} + onClick={() => setEditingAcceptorAmount(!editingAcceptorAmount)} + > + Edit + </Button> + )} + <Button + type="submit" + color={'indigo'} + className={clsx( + 'whitespace-nowrap drop-shadow-md', + isCreating ? 'disabled' : '' + )} + > + Continue + </Button> + </Row> + <Row className={'text-error'}>{error} </Row> + </form> + )} + {finishedCreating && ( + <> + <Title className="!my-0" text="Challenge Created!" /> + + <div>Share the challenge using the link.</div> + <button + onClick={() => { + copyToClipboard(challengeSlug) + toast('Link copied to clipboard!') + }} + className={'btn btn-outline mb-4 whitespace-nowrap normal-case'} + > + <LinkIcon className={'mr-2 h-5 w-5'} /> + Copy link + </button> + + <QRCode url={challengeSlug} className="self-center" /> + <Row className={'gap-1 text-gray-500'}> + See your other + <SiteLink className={'underline'} href={'/challenges'}> + challenges + </SiteLink> + </Row> + </> + )} + </> + ) +} diff --git a/web/components/contract/contract-overview.tsx b/web/components/contract/contract-overview.tsx index b95bb02b..79d05ec2 100644 --- a/web/components/contract/contract-overview.tsx +++ b/web/components/contract/contract-overview.tsx @@ -1,7 +1,11 @@ +<<<<<<< HEAD import React from 'react' import clsx from 'clsx' import { tradingAllowed } from 'web/lib/firebase/contracts' +======= +import { contractUrl, tradingAllowed } from 'web/lib/firebase/contracts' +>>>>>>> 798253f8 (Challenge Bets (#679)) import { Col } from '../layout/col' import { Spacer } from '../layout/spacer' import { ContractProbGraph } from './contract-prob-graph' @@ -21,7 +25,16 @@ import { Contract, CPMMBinaryContract } from 'common/contract' import { ContractDescription } from './contract-description' import { ContractDetails } from './contract-details' import { NumericGraph } from './numeric-graph' +<<<<<<< HEAD import { ShareRow } from './share-row' +======= +import { CreateChallengeButton } from 'web/components/challenges/create-challenge-button' +import React from 'react' +import { copyToClipboard } from 'web/lib/util/copy' +import toast from 'react-hot-toast' +import { LinkIcon } from '@heroicons/react/outline' +import { CHALLENGES_ENABLED } from 'common/challenge' +>>>>>>> 798253f8 (Challenge Bets (#679)) export const ContractOverview = (props: { contract: Contract @@ -36,6 +49,7 @@ export const ContractOverview = (props: { const isBinary = outcomeType === 'BINARY' const isPseudoNumeric = outcomeType === 'PSEUDO_NUMERIC' + const showChallenge = user && isBinary && !resolution && CHALLENGES_ENABLED return ( <Col className={clsx('mb-6', className)}> @@ -118,12 +132,51 @@ export const ContractOverview = (props: { <AnswersGraph contract={contract} bets={bets} /> )} {outcomeType === 'NUMERIC' && <NumericGraph contract={contract} />} +<<<<<<< HEAD <ShareRow user={user} contract={contract} /> +======= + {/* {(contract.description || isCreator) && <Spacer h={6} />} */} +>>>>>>> 798253f8 (Challenge Bets (#679)) <ContractDescription className="px-2" contract={contract} isCreator={isCreator} /> + {/*<Row className="mx-4 mt-4 hidden justify-around sm:block">*/} + {/* {showChallenge && (*/} + {/* <Col className="gap-3">*/} + {/* <div className="text-lg">⚔️ Challenge a friend ⚔️</div>*/} + {/* <CreateChallengeButton user={user} contract={contract} />*/} + {/* </Col>*/} + {/* )}*/} + {/* {isCreator && (*/} + {/* <Col className="gap-3">*/} + {/* <div className="text-lg">Share your market</div>*/} + {/* <ShareMarketButton contract={contract} />*/} + {/* </Col>*/} + {/* )}*/} + {/*</Row>*/} + <Row className="mx-4 mt-6 block justify-around"> + {showChallenge && ( + <Col className="gap-3"> + <CreateChallengeButton user={user} contract={contract} /> + </Col> + )} + {isCreator && ( + <Col className="gap-3"> + <button + onClick={() => { + copyToClipboard(contractUrl(contract)) + toast('Link copied to clipboard!') + }} + className={'btn btn-outline mb-4 whitespace-nowrap normal-case'} + > + <LinkIcon className={'mr-2 h-5 w-5'} /> + Share market + </button> + </Col> + )} + </Row> </Col> ) } diff --git a/web/pages/challenges/index.tsx b/web/pages/challenges/index.tsx index 9553bb95..343611a7 100644 --- a/web/pages/challenges/index.tsx +++ b/web/pages/challenges/index.tsx @@ -27,6 +27,11 @@ import { Button } from 'web/components/button' import { ClipboardCopyIcon, QrcodeIcon } from '@heroicons/react/outline' import { copyToClipboard } from 'web/lib/util/copy' import toast from 'react-hot-toast' +<<<<<<< HEAD +======= +import { Modal } from 'web/components/layout/modal' +import { QRCode } from 'web/components/qr-code' +>>>>>>> 798253f8 (Challenge Bets (#679)) dayjs.extend(customParseFormat) const columnClass = 'sm:px-5 px-2 py-3.5 max-w-[100px] truncate' @@ -107,11 +112,16 @@ function YourChallengesTable(props: { links: Challenge[] }) { function YourLinkSummaryRow(props: { challenge: Challenge }) { const { challenge } = props const { acceptances } = challenge +<<<<<<< HEAD +======= + const [open, setOpen] = React.useState(false) +>>>>>>> 798253f8 (Challenge Bets (#679)) const className = clsx( 'whitespace-nowrap text-sm hover:cursor-pointer text-gray-500 hover:bg-sky-50 bg-white' ) return ( +<<<<<<< HEAD <tr id={challenge.slug} key={challenge.slug} className={className}> <td className={amountClass}> <SiteLink href={getChallengeUrl(challenge)}> @@ -180,6 +190,87 @@ function YourLinkSummaryRow(props: { challenge: Challenge }) { </Row> </td> </tr> +======= + <> + <Modal open={open} setOpen={setOpen} size={'sm'}> + <Col + className={ + 'items-center justify-center gap-4 rounded-md bg-white p-8 py-8 ' + } + > + <span className={'mb-4 text-center text-xl text-indigo-700'}> + Have your friend scan this to accept the challenge! + </span> + <QRCode url={getChallengeUrl(challenge)} /> + </Col> + </Modal> + <tr id={challenge.slug} key={challenge.slug} className={className}> + <td className={amountClass}> + <SiteLink href={getChallengeUrl(challenge)}> + {formatMoney(challenge.creatorAmount)} + </SiteLink> + </td> + <td + className={clsx( + columnClass, + 'text-center sm:max-w-[200px] sm:text-start' + )} + > + <Row className="items-center gap-2"> + <Button + color="gray-white" + size="xs" + onClick={() => { + copyToClipboard(getChallengeUrl(challenge)) + toast('Link copied to clipboard!') + }} + > + <ClipboardCopyIcon className={'h-5 w-5 sm:h-4 sm:w-4'} /> + </Button> + <Button + color="gray-white" + size="xs" + onClick={() => { + setOpen(true) + }} + > + <QrcodeIcon className="h-5 w-5 sm:h-4 sm:w-4" /> + </Button> + <SiteLink + href={getChallengeUrl(challenge)} + className={'mx-1 mb-1 hidden sm:inline-block'} + > + {`...${challenge.contractSlug}/${challenge.slug}`} + </SiteLink> + </Row> + </td> + + <td className={columnClass}> + <Row className={'items-center justify-start gap-1'}> + {acceptances.length > 0 ? ( + <> + <Avatar + username={acceptances[0].userUsername} + avatarUrl={acceptances[0].userAvatarUrl} + size={'sm'} + /> + <UserLink + name={acceptances[0].userName} + username={acceptances[0].userUsername} + /> + </> + ) : ( + <span> + No one - + {challenge.expiresTime && + ` (expires ${fromNow(challenge.expiresTime)})`} + </span> + )} + </Row> + </td> + </tr> + </> +>>>>>>> 798253f8 (Challenge Bets (#679)) ) }