diff --git a/web/components/button.tsx b/web/components/button.tsx index 5c1e15f8..462670bd 100644 --- a/web/components/button.tsx +++ b/web/components/button.tsx @@ -52,7 +52,7 @@ export function Button(props: { color === 'gradient' && 'bg-gradient-to-r from-indigo-500 to-blue-500 text-white hover:from-indigo-700 hover:to-blue-700', color === 'gray-white' && - 'text-greyscale-6 hover:bg-greyscale-2 bg-white', + 'border-none bg-white text-gray-500 shadow-none hover:bg-gray-200', className )} disabled={disabled} diff --git a/web/components/challenges/create-challenge-modal.tsx b/web/components/challenges/create-challenge-modal.tsx new file mode 100644 index 00000000..3a0e857a --- /dev/null +++ b/web/components/challenges/create-challenge-modal.tsx @@ -0,0 +1,248 @@ +import clsx from 'clsx' +import dayjs from 'dayjs' +import React, { useEffect, useState } from 'react' +import { LinkIcon, SwitchVerticalIcon } from '@heroicons/react/outline' +import toast from 'react-hot-toast' + +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' + +type challengeInfo = { + amount: number + expiresTime: number | null + message: string + outcome: 'YES' | 'NO' | number + acceptorAmount: number +} + +export function CreateChallengeModal(props: { + user: User | null | undefined + contract: BinaryContract + isOpen: boolean + setOpen: (open: boolean) => void +}) { + const { user, contract, isOpen, setOpen } = props + const [challengeSlug, setChallengeSlug] = useState('') + + return ( + + + {/*// 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) + }} + > + + + You'll bet: + + + + + M$ + + + setChallengeInfo((m: challengeInfo) => { + return { + ...m, + amount: parseInt(e.target.value), + acceptorAmount: editingAcceptorAmount + ? m.acceptorAmount + : parseInt(e.target.value), + } + }) + } + /> + + + on + {challengeInfo.outcome === 'YES' ? : } + + + + setChallengeInfo((m: challengeInfo) => { + return { + ...m, + outcome: m.outcome === 'YES' ? 'NO' : 'YES', + } + }) + } + > + + + + If they bet: + + + {editingAcceptorAmount ? ( + + + + M$ + + + setChallengeInfo((m: challengeInfo) => { + return { + ...m, + acceptorAmount: parseInt(e.target.value), + } + }) + } + /> + + + ) : ( + + {formatMoney(challengeInfo.acceptorAmount)} + + )} + + on + {challengeInfo.outcome === 'YES' ? : } + + + + {!editingAcceptorAmount && ( + setEditingAcceptorAmount(!editingAcceptorAmount)} + > + Edit + + )} + + Continue + + + {error} + + )} + {finishedCreating && ( + <> + + + Share the challenge using the link. + { + copyToClipboard(challengeSlug) + toast('Link copied to clipboard!') + }} + className={'btn btn-outline mb-4 whitespace-nowrap normal-case'} + > + + Copy link + + + + + See your other + + challenges + + + > + )} + > + ) +} diff --git a/web/components/contract/contract-details.tsx b/web/components/contract/contract-details.tsx index 7a7242a0..9d12496d 100644 --- a/web/components/contract/contract-details.tsx +++ b/web/components/contract/contract-details.tsx @@ -5,13 +5,13 @@ import { TrendingUpIcon, UserGroupIcon, } from '@heroicons/react/outline' + import { Row } from '../layout/row' import { formatMoney } from 'common/util/format' import { UserLink } from '../user-page' import { Contract, contractMetrics, - contractPath, updateContract, } from 'web/lib/firebase/contracts' import dayjs from 'dayjs' @@ -24,11 +24,9 @@ import { Bet } from 'common/bet' import NewContractBadge from '../new-contract-badge' import { UserFollowButton } from '../follow-button' import { DAY_MS } from 'common/util/time' -import { ShareIconButton } from 'web/components/share-icon-button' import { useUser } from 'web/hooks/use-user' import { Editor } from '@tiptap/react' import { exhibitExts } from 'common/util/parse' -import { ENV_CONFIG } from 'common/envs/constants' import { Button } from 'web/components/button' import { Modal } from 'web/components/layout/modal' import { Col } from 'web/components/layout/col' @@ -228,14 +226,6 @@ export function ContractDetails(props: { {volumeLabel} - {!disabled && } diff --git a/web/components/contract/contract-info-dialog.tsx b/web/components/contract/contract-info-dialog.tsx index a1f79479..168ada50 100644 --- a/web/components/contract/contract-info-dialog.tsx +++ b/web/components/contract/contract-info-dialog.tsx @@ -7,16 +7,12 @@ import { Bet } from 'common/bet' import { Contract } from 'common/contract' import { formatMoney } from 'common/util/format' -import { contractPath, contractPool } from 'web/lib/firebase/contracts' +import { contractPool } from 'web/lib/firebase/contracts' import { LiquidityPanel } from '../liquidity-panel' import { Col } from '../layout/col' import { Modal } from '../layout/modal' -import { Row } from '../layout/row' -import { ShareEmbedButton } from '../share-embed-button' import { Title } from '../title' -import { TweetButton } from '../tweet-button' import { InfoTooltip } from '../info-tooltip' -import { DuplicateContractButton } from '../copy-contract-button' 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' @@ -61,20 +57,6 @@ export function ContractInfoDialog(props: { contract: Contract; bets: Bet[] }) { - Share - - - - - - - - - Stats - @@ -150,14 +132,3 @@ export function ContractInfoDialog(props: { contract: Contract; bets: Bet[] }) { > ) } - -const getTweetText = (contract: Contract) => { - const { question, resolution } = contract - - const tweetDescription = resolution ? `\n\nResolved ${resolution}!` : '' - - const timeParam = `${Date.now()}`.substring(7) - const url = `https://manifold.markets${contractPath(contract)}?t=${timeParam}` - - return `${question}\n\n${url}${tweetDescription}` -} diff --git a/web/components/contract/contract-overview.tsx b/web/components/contract/contract-overview.tsx index 28eabb04..b95bb02b 100644 --- a/web/components/contract/contract-overview.tsx +++ b/web/components/contract/contract-overview.tsx @@ -1,12 +1,13 @@ -import { contractUrl, tradingAllowed } from 'web/lib/firebase/contracts' +import React from 'react' +import clsx from 'clsx' + +import { tradingAllowed } from 'web/lib/firebase/contracts' import { Col } from '../layout/col' import { Spacer } from '../layout/spacer' import { ContractProbGraph } from './contract-prob-graph' import { useUser } from 'web/hooks/use-user' import { Row } from '../layout/row' import { Linkify } from '../linkify' -import clsx from 'clsx' - import { BinaryResolutionOrChance, FreeResponseResolutionOrChance, @@ -20,12 +21,7 @@ import { Contract, CPMMBinaryContract } from 'common/contract' import { ContractDescription } from './contract-description' import { ContractDetails } from './contract-details' import { NumericGraph } from './numeric-graph' -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' +import { ShareRow } from './share-row' export const ContractOverview = (props: { contract: Contract @@ -40,7 +36,6 @@ export const ContractOverview = (props: { const isBinary = outcomeType === 'BINARY' const isPseudoNumeric = outcomeType === 'PSEUDO_NUMERIC' - const showChallenge = user && isBinary && !resolution && CHALLENGES_ENABLED return ( @@ -123,47 +118,12 @@ export const ContractOverview = (props: { )} {outcomeType === 'NUMERIC' && } - {/* {(contract.description || isCreator) && } */} + - {/**/} - {/* {showChallenge && (*/} - {/* */} - {/* ⚔️ Challenge a friend ⚔️*/} - {/* */} - {/* */} - {/* )}*/} - {/* {isCreator && (*/} - {/* */} - {/* Share your market*/} - {/* */} - {/* */} - {/* )}*/} - {/**/} - - {showChallenge && ( - - - - )} - {isCreator && ( - - { - copyToClipboard(contractUrl(contract)) - toast('Link copied to clipboard!') - }} - className={'btn btn-outline mb-4 whitespace-nowrap normal-case'} - > - - Share market - - - )} - ) } diff --git a/web/components/contract/share-modal.tsx b/web/components/contract/share-modal.tsx new file mode 100644 index 00000000..017d3174 --- /dev/null +++ b/web/components/contract/share-modal.tsx @@ -0,0 +1,77 @@ +import { LinkIcon } from '@heroicons/react/outline' +import toast from 'react-hot-toast' + +import { Contract } from 'common/contract' +import { contractPath } from 'web/lib/firebase/contracts' +import { Col } from '../layout/col' +import { Modal } from '../layout/modal' +import { Row } from '../layout/row' +import { ShareEmbedButton } from '../share-embed-button' +import { Title } from '../title' +import { TweetButton } from '../tweet-button' +import { DuplicateContractButton } from '../copy-contract-button' +import { Button } from '../button' +import { copyToClipboard } from 'web/lib/util/copy' +import { track } from 'web/lib/service/analytics' +import { ENV_CONFIG } from 'common/envs/constants' +import { User } from 'common/user' + +export function ShareModal(props: { + contract: Contract + user: User | undefined | null + isOpen: boolean + setOpen: (open: boolean) => void +}) { + const { contract, user, isOpen, setOpen } = props + + const linkIcon = + + const copyPayload = `https://${ENV_CONFIG.domain}${contractPath(contract)}${ + user?.username && contract.creatorUsername !== user?.username + ? '?referrer=' + user?.username + : '' + }` + + return ( + + + + + { + copyToClipboard(copyPayload) + track('copy share link') + toast.success('Link copied!', { + icon: linkIcon, + }) + }} + > + {linkIcon} Copy link + + + + + + + + + + ) +} + +const getTweetText = (contract: Contract) => { + const { question, resolution } = contract + + const tweetDescription = resolution ? `\n\nResolved ${resolution}!` : '' + + const timeParam = `${Date.now()}`.substring(7) + const url = `https://manifold.markets${contractPath(contract)}?t=${timeParam}` + + return `${question}\n\n${url}${tweetDescription}` +} diff --git a/web/components/contract/share-row.tsx b/web/components/contract/share-row.tsx new file mode 100644 index 00000000..fd872c5a --- /dev/null +++ b/web/components/contract/share-row.tsx @@ -0,0 +1,59 @@ +import clsx from 'clsx' +import { ShareIcon } from '@heroicons/react/outline' + +import { Row } from '../layout/row' +import { Contract } from 'web/lib/firebase/contracts' +import { useState } from 'react' +import { Button } from 'web/components/button' +import { CreateChallengeModal } from '../challenges/create-challenge-modal' +import { User } from 'common/user' +import { CHALLENGES_ENABLED } from 'common/challenge' +import { ShareModal } from './share-modal' + +export function ShareRow(props: { + contract: Contract + user: User | undefined | null +}) { + 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) + + return ( + + { + setShareOpen(true) + }} + > + + Share + + + + {showChallenge && ( + setIsOpen(true)}> + ⚔️ Challenge + + + )} + + ) +} diff --git a/web/pages/challenges/index.tsx b/web/pages/challenges/index.tsx index 7c68f0bd..e548e56f 100644 --- a/web/pages/challenges/index.tsx +++ b/web/pages/challenges/index.tsx @@ -113,6 +113,7 @@ function YourChallengesTable(props: { links: Challenge[] }) { function YourLinkSummaryRow(props: { challenge: Challenge }) { const { challenge } = props const { acceptances } = challenge + const [open, setOpen] = React.useState(false) const className = clsx( 'whitespace-nowrap text-sm hover:cursor-pointer text-gray-500 hover:bg-sky-50 bg-white'