diff --git a/web/components/button.tsx b/web/components/button.tsx index af5ad007..8e00a077 100644 --- a/web/components/button.tsx +++ b/web/components/button.tsx @@ -82,3 +82,39 @@ export function Button(props: { ) } + +export function IconButton(props: { + className?: string + onClick?: MouseEventHandler | undefined + children?: ReactNode + size?: SizeType + type?: 'button' | 'reset' | 'submit' + disabled?: boolean + loading?: boolean +}) { + const { + children, + className, + onClick, + size = 'md', + type = 'button', + disabled = false, + loading, + } = props + + return ( + + ) +} diff --git a/web/components/contract/contract-info-dialog.tsx b/web/components/contract/contract-info-dialog.tsx index a41be451..3ddeccac 100644 --- a/web/components/contract/contract-info-dialog.tsx +++ b/web/components/contract/contract-info-dialog.tsx @@ -19,11 +19,9 @@ import ShortToggle from '../widgets/short-toggle' import { DuplicateContractButton } from '../duplicate-contract-button' import { Row } from '../layout/row' import { BETTORS, User } from 'common/user' -import { Button } from '../button' +import { IconButton } from '../button' import { AddLiquidityButton } from './add-liquidity-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' +import { Tooltip } from '../tooltip' export function ContractInfoDialog(props: { contract: Contract @@ -84,171 +82,173 @@ export function ContractInfoDialog(props: { return ( <> - + + setOpen(true)} + > + - - - + <Modal open={open} setOpen={setOpen}> + <Col className="gap-4 rounded bg-white p-6"> + <Title className="!mt-0 !mb-0" text="This Market" /> - <table className="table-compact table-zebra table w-full text-gray-500"> - <tbody> - <tr> - <td>Type</td> - <td>{typeDisplay}</td> - </tr> - - <tr> - <td>Payout</td> - <td className="flex gap-1"> - {mechanism === 'cpmm-1' ? ( - <> - Fixed{' '} - <InfoTooltip text="Each YES share is worth M$1 if YES wins." /> - </> - ) : ( - <> - Parimutuel{' '} - <InfoTooltip text="Each share is a fraction of the pool. " /> - </> - )} - </td> - </tr> - - <tr> - <td>Market created</td> - <td>{formatTime(createdTime)}</td> - </tr> - - {closeTime && ( + <table className="table-compact table-zebra table w-full text-gray-500"> + <tbody> <tr> - <td>Market close{closeTime > Date.now() ? 's' : 'd'}</td> - <td>{formatTime(closeTime)}</td> + <td>Type</td> + <td>{typeDisplay}</td> </tr> - )} - {resolutionTime && ( <tr> - <td>Market resolved</td> - <td>{formatTime(resolutionTime)}</td> - </tr> - )} - - <tr> - <td> - <span className="mr-1">Volume</span> - <InfoTooltip text="Total amount bought or sold" /> - </td> - <td>{formatMoney(contract.volume)}</td> - </tr> - - <tr> - <td>{capitalize(BETTORS)}</td> - <td>{uniqueBettorCount ?? '0'}</td> - </tr> - - <tr> - <td> - <Row> - <span className="mr-1">Elasticity</span> - <InfoTooltip - text={ - mechanism === 'cpmm-1' - ? 'Probability change between a M$50 bet on YES and NO' - : 'Probability change from a M$100 bet' - } - /> - </Row> - </td> - <td>{formatPercent(elasticity)}</td> - </tr> - - <tr> - <td>Liquidity subsidies</td> - <td> - {mechanism === 'cpmm-1' - ? formatMoney(contract.totalLiquidity) - : formatMoney(100)} - </td> - </tr> - - <tr> - <td>Pool</td> - <td> - {mechanism === 'cpmm-1' && outcomeType === 'BINARY' - ? `${Math.round(pool.YES)} YES, ${Math.round(pool.NO)} NO` - : mechanism === 'cpmm-1' && outcomeType === 'PSEUDO_NUMERIC' - ? `${Math.round(pool.YES)} HIGHER, ${Math.round( - pool.NO - )} LOWER` - : contractPool(contract)} - </td> - </tr> - - {/* Show a path to Firebase if user is an admin, or we're on localhost */} - {(isAdmin || isDev) && ( - <tr> - <td>[ADMIN] Firestore</td> - <td> - <SiteLink href={firestoreConsolePath(id)}> - Console link - </SiteLink> + <td>Payout</td> + <td className="flex gap-1"> + {mechanism === 'cpmm-1' ? ( + <> + Fixed{' '} + <InfoTooltip text="Each YES share is worth M$1 if YES wins." /> + </> + ) : ( + <> + Parimutuel{' '} + <InfoTooltip text="Each share is a fraction of the pool. " /> + </> + )} </td> </tr> - )} - {isAdmin && ( - <tr> - <td>[ADMIN] Featured</td> - <td> - <ShortToggle - on={featured} - setOn={setFeatured} - onChange={onFeaturedToggle} - /> - </td> - </tr> - )} - {user && ( - <tr> - <td>{isAdmin ? '[ADMIN]' : ''} Unlisted</td> - <td> - <ShortToggle - disabled={ - isUnlisted - ? !(isAdmin || (isCreator && wasUnlistedByCreator)) - : !(isCreator || isAdmin) - } - on={contract.visibility === 'unlisted'} - setOn={(b) => - updateContract(id, { - visibility: b ? 'unlisted' : 'public', - unlistedById: b ? user.id : '', - }) - } - /> - </td> - </tr> - )} - </tbody> - </table> - <Row className="flex-wrap"> - {mechanism === 'cpmm-1' && ( - <AddLiquidityButton contract={contract} className="mr-2" /> - )} - <DuplicateContractButton contract={contract} /> - </Row> - </Col> - </Modal> + <tr> + <td>Market created</td> + <td>{formatTime(createdTime)}</td> + </tr> + + {closeTime && ( + <tr> + <td>Market close{closeTime > Date.now() ? 's' : 'd'}</td> + <td>{formatTime(closeTime)}</td> + </tr> + )} + + {resolutionTime && ( + <tr> + <td>Market resolved</td> + <td>{formatTime(resolutionTime)}</td> + </tr> + )} + + <tr> + <td> + <span className="mr-1">Volume</span> + <InfoTooltip text="Total amount bought or sold" /> + </td> + <td>{formatMoney(contract.volume)}</td> + </tr> + + <tr> + <td>{capitalize(BETTORS)}</td> + <td>{uniqueBettorCount ?? '0'}</td> + </tr> + + <tr> + <td> + <Row> + <span className="mr-1">Elasticity</span> + <InfoTooltip + text={ + mechanism === 'cpmm-1' + ? 'Probability change between a M$50 bet on YES and NO' + : 'Probability change from a M$100 bet' + } + /> + </Row> + </td> + <td>{formatPercent(elasticity)}</td> + </tr> + + <tr> + <td>Liquidity subsidies</td> + <td> + {mechanism === 'cpmm-1' + ? formatMoney(contract.totalLiquidity) + : formatMoney(100)} + </td> + </tr> + + <tr> + <td>Pool</td> + <td> + {mechanism === 'cpmm-1' && outcomeType === 'BINARY' + ? `${Math.round(pool.YES)} YES, ${Math.round(pool.NO)} NO` + : mechanism === 'cpmm-1' && + outcomeType === 'PSEUDO_NUMERIC' + ? `${Math.round(pool.YES)} HIGHER, ${Math.round( + pool.NO + )} LOWER` + : contractPool(contract)} + </td> + </tr> + + {/* Show a path to Firebase if user is an admin, or we're on localhost */} + {(isAdmin || isDev) && ( + <tr> + <td>[ADMIN] Firestore</td> + <td> + <SiteLink href={firestoreConsolePath(id)}> + Console link + </SiteLink> + </td> + </tr> + )} + {isAdmin && ( + <tr> + <td>[ADMIN] Featured</td> + <td> + <ShortToggle + on={featured} + setOn={setFeatured} + onChange={onFeaturedToggle} + /> + </td> + </tr> + )} + {user && ( + <tr> + <td>{isAdmin ? '[ADMIN]' : ''} Unlisted</td> + <td> + <ShortToggle + disabled={ + isUnlisted + ? !(isAdmin || (isCreator && wasUnlistedByCreator)) + : !(isCreator || isAdmin) + } + on={contract.visibility === 'unlisted'} + setOn={(b) => + updateContract(id, { + visibility: b ? 'unlisted' : 'public', + unlistedById: b ? user.id : '', + }) + } + /> + </td> + </tr> + )} + </tbody> + </table> + + <Row className="flex-wrap"> + {mechanism === 'cpmm-1' && ( + <AddLiquidityButton contract={contract} className="mr-2" /> + )} + <DuplicateContractButton contract={contract} /> + </Row> + </Col> + </Modal> + </Tooltip> </> ) } diff --git a/web/components/contract/extra-contract-actions-row.tsx b/web/components/contract/extra-contract-actions-row.tsx index 0c77c666..7353bb6e 100644 --- a/web/components/contract/extra-contract-actions-row.tsx +++ b/web/components/contract/extra-contract-actions-row.tsx @@ -2,7 +2,7 @@ import { ShareIcon } from '@heroicons/react/outline' import { Row } from '../layout/row' import { Contract } from 'web/lib/firebase/contracts' import React, { useState } from 'react' -import { Button } from 'web/components/button' +import { IconButton } from 'web/components/button' import { useUser } from 'web/hooks/use-user' import { ShareModal } from './share-modal' import { FollowMarketButton } from 'web/components/follow-market-button' @@ -16,15 +16,14 @@ export function ExtraContractActionsRow(props: { contract: Contract }) { const [isShareOpen, setShareOpen] = useState(false) return ( - <Row> + <Row className="gap-1"> <FollowMarketButton contract={contract} user={user} /> <LikeMarketButton contract={contract} user={user} /> <Tooltip text="Share" placement="bottom" noTap noFade> - <Button - size="sm" - color="gray-white" + <IconButton + size="2xs" className={'flex'} onClick={() => setShareOpen(true)} > @@ -35,7 +34,7 @@ export function ExtraContractActionsRow(props: { contract: Contract }) { contract={contract} user={user} /> - </Button> + </IconButton> </Tooltip> <ContractInfoDialog contract={contract} user={user} /> diff --git a/web/components/contract/tip-button.tsx b/web/components/contract/tip-button.tsx index df245c68..fd80d97f 100644 --- a/web/components/contract/tip-button.tsx +++ b/web/components/contract/tip-button.tsx @@ -1,10 +1,9 @@ import clsx from 'clsx' -import { HeartIcon } from '@heroicons/react/outline' - -import { Button } from 'web/components/button' import { formatMoney, shortFormatNumber } from 'common/util/format' import { Col } from 'web/components/layout/col' import { Tooltip } from '../tooltip' +import TipJar from 'web/public/custom-components/tipJar' +import { useState } from 'react' export function TipButton(props: { tipAmount: number @@ -14,11 +13,12 @@ export function TipButton(props: { isCompact?: boolean disabled?: boolean }) { - const { tipAmount, totalTipped, userTipped, isCompact, onClick, disabled } = - props + const { tipAmount, totalTipped, userTipped, onClick, disabled } = props const tipDisplay = shortFormatNumber(Math.ceil(totalTipped / 10)) + const [hover, setHover] = useState(false) + return ( <Tooltip text={ @@ -30,39 +30,40 @@ export function TipButton(props: { noTap noFade > - <Button - size={'sm'} - className={clsx( - 'max-w-xs self-center', - isCompact && 'px-0 py-0', - disabled && 'hover:bg-inherit' - )} - color={'gray-white'} + <button onClick={onClick} disabled={disabled} + className={clsx( + 'px-2 py-1 text-xs', //2xs button + 'text-greyscale-6 transition-transform hover:text-indigo-600 disabled:cursor-not-allowed', + !disabled ? 'hover:rotate-12' : '' + )} + onMouseOver={() => setHover(true)} + onMouseLeave={() => setHover(false)} > - <Col className={'relative items-center sm:flex-row'}> - <HeartIcon - className={clsx( - 'h-5 w-5', - totalTipped > 0 ? 'mr-2' : '', - userTipped ? 'fill-teal-500 text-teal-500' : '' - )} + <Col className={clsx('relative', disabled ? 'opacity-30' : '')}> + <TipJar + size={18} + color={userTipped || (hover && !disabled) ? '#4f46e5' : '#66667C'} + fill={userTipped ? '#4f46e5' : 'none'} /> - {totalTipped > 0 && ( - <div - className={clsx( - 'bg-greyscale-5 absolute ml-3.5 mt-2 h-4 w-4 rounded-full align-middle text-white sm:mt-3 sm:h-5 sm:w-5 sm:px-1', - tipDisplay.length > 2 - ? 'text-[0.4rem] sm:text-[0.5rem]' - : 'sm:text-2xs text-[0.5rem]' - )} - > - {tipDisplay} - </div> - )} + <div + className={clsx( + ' absolute top-[2px] text-[0.5rem]', + userTipped ? 'text-white' : '', + tipDisplay.length === 1 + ? 'left-[7px]' + : tipDisplay.length === 2 + ? 'left-[4.5px]' + : tipDisplay.length > 2 + ? 'left-[4px] top-[2.5px] text-[0.35rem]' + : '' + )} + > + {totalTipped > 0 ? tipDisplay : ''} + </div> </Col> - </Button> + </button> </Tooltip> ) } diff --git a/web/components/feed/feed-comments.tsx b/web/components/feed/feed-comments.tsx index 9bdeed53..73d89c67 100644 --- a/web/components/feed/feed-comments.tsx +++ b/web/components/feed/feed-comments.tsx @@ -24,7 +24,7 @@ import { UserLink } from 'web/components/user-link' import { CommentInput } from '../comment-input' import { AwardBountyButton } from 'web/components/award-bounty-button' import { ReplyIcon } from '@heroicons/react/solid' -import { Button } from '../button' +import { IconButton } from '../button' import { ReplyToggle } from '../comments/reply-toggle' export type ReplyTo = { id: string; username: string } @@ -154,36 +154,46 @@ export function ParentFeedComment(props: { numComments={numComments} onClick={onSeeReplyClick} /> - <Row className="grow justify-end gap-2"> - {onReplyClick && ( - <Button - size={'sm'} - className={clsx( - 'hover:bg-greyscale-2 mt-0 mb-1 max-w-xs px-0 py-0' - )} - color={'gray-white'} - onClick={() => onReplyClick(comment)} - > - <ReplyIcon className="h-5 w-5" /> - </Button> - )} - {showTip && ( - <Tipper - comment={comment} - myTip={myTip ?? 0} - totalTip={totalTip ?? 0} - /> - )} - {(contract.openCommentBounties ?? 0) > 0 && ( - <AwardBountyButton comment={comment} contract={contract} /> - )} - </Row> + <CommentActions + onReplyClick={onReplyClick} + comment={comment} + showTip={showTip} + myTip={myTip} + totalTip={totalTip} + contract={contract} + /> </Row> </Col> </Row> ) } +export function CommentActions(props: { + onReplyClick?: (comment: ContractComment) => void + comment: ContractComment + showTip?: boolean + myTip?: number + totalTip?: number + contract: Contract +}) { + const { onReplyClick, comment, showTip, myTip, totalTip, contract } = props + return ( + <Row className="grow justify-end"> + {onReplyClick && ( + <IconButton size={'xs'} onClick={() => onReplyClick(comment)}> + <ReplyIcon className="h-5 w-5" /> + </IconButton> + )} + {showTip && ( + <Tipper comment={comment} myTip={myTip ?? 0} totalTip={totalTip ?? 0} /> + )} + {(contract.openCommentBounties ?? 0) > 0 && ( + <AwardBountyButton comment={comment} contract={contract} /> + )} + </Row> + ) +} + export const FeedComment = memo(function FeedComment(props: { contract: Contract comment: ContractComment @@ -233,30 +243,14 @@ export const FeedComment = memo(function FeedComment(props: { content={content || text} smallImage /> - <Row className="grow justify-end gap-2"> - {onReplyClick && ( - <Button - size={'sm'} - className={clsx( - 'hover:bg-greyscale-2 mt-0 mb-1 max-w-xs px-0 py-0' - )} - color={'gray-white'} - onClick={() => onReplyClick(comment)} - > - <ReplyIcon className="h-5 w-5" /> - </Button> - )} - {showTip && ( - <Tipper - comment={comment} - myTip={myTip ?? 0} - totalTip={totalTip ?? 0} - /> - )} - {(contract.openCommentBounties ?? 0) > 0 && ( - <AwardBountyButton comment={comment} contract={contract} /> - )} - </Row> + <CommentActions + onReplyClick={onReplyClick} + comment={comment} + showTip={showTip} + myTip={myTip} + totalTip={totalTip} + contract={contract} + /> </Col> </Row> ) diff --git a/web/components/follow-market-button.tsx b/web/components/follow-market-button.tsx index 319d4af6..5185390b 100644 --- a/web/components/follow-market-button.tsx +++ b/web/components/follow-market-button.tsx @@ -1,4 +1,4 @@ -import { Button } from 'web/components/button' +import { IconButton } from 'web/components/button' import { Contract, followContract, @@ -33,9 +33,8 @@ export const FollowMarketButton = (props: { noTap noFade > - <Button - size={'sm'} - color={'gray-white'} + <IconButton + size="2xs" onClick={async () => { if (!user) return firebaseLogin() if (followers?.includes(user.id)) { @@ -65,18 +64,12 @@ export const FollowMarketButton = (props: { > {watching ? ( <Col className={'items-center gap-x-2 sm:flex-row'}> - <EyeOffIcon - className={clsx('h-5 w-5 sm:h-6 sm:w-6')} - aria-hidden="true" - /> + <EyeOffIcon className={clsx('h-5 w-5')} aria-hidden="true" /> {/* Unwatch */} </Col> ) : ( <Col className={'items-center gap-x-2 sm:flex-row'}> - <EyeIcon - className={clsx('h-5 w-5 sm:h-6 sm:w-6')} - aria-hidden="true" - /> + <EyeIcon className={clsx('h-5 w-5')} aria-hidden="true" /> {/* Watch */} </Col> )} @@ -87,7 +80,7 @@ export const FollowMarketButton = (props: { followers?.includes(user?.id ?? 'nope') ? 'watched' : 'unwatched' } a question!`} /> - </Button> + </IconButton> </Tooltip> ) } diff --git a/web/components/manalink-card.tsx b/web/components/manalink-card.tsx index b04fd0da..62b39d74 100644 --- a/web/components/manalink-card.tsx +++ b/web/components/manalink-card.tsx @@ -9,9 +9,9 @@ import { Col } from 'web/components/layout/col' import { Row } from 'web/components/layout/row' import { Claim, Manalink } from 'common/manalink' import { ShareIconButton } from './share-icon-button' -import { contractDetailsButtonClassName } from './contract/contract-info-dialog' import { useUserById } from 'web/hooks/use-user' import getManalinkUrl from 'web/get-manalink-url' +import { IconButton } from './button' export type ManalinkInfo = { expiresTime: number | null @@ -123,7 +123,7 @@ export function ManalinkCardFromView(props: { src="/logo-white.svg" /> </Col> - <Row className="relative w-full gap-1 rounded-b-lg bg-white px-4 py-2 text-lg"> + <Row className="relative w-full rounded-b-lg bg-white px-4 py-2 align-middle text-lg"> <div className={clsx( 'my-auto mb-1 w-full', @@ -133,32 +133,23 @@ export function ManalinkCardFromView(props: { {formatMoney(amount)} </div> - <button - onClick={() => (window.location.href = qrUrl)} - className={clsx(contractDetailsButtonClassName)} - > + <IconButton size="2xs" onClick={() => (window.location.href = qrUrl)}> <QrcodeIcon className="h-6 w-6" /> - </button> + </IconButton> <ShareIconButton toastClassName={'-left-48 min-w-[250%]'} - buttonClassName={'transition-colors'} - onCopyButtonClassName={ - 'bg-gray-200 text-gray-600 transition-none hover:bg-gray-200 hover:text-gray-600' - } copyPayload={getManalinkUrl(link.slug)} /> - <button + <IconButton + size="xs" onClick={() => setShowDetails(!showDetails)} className={clsx( - contractDetailsButtonClassName, - showDetails - ? 'bg-gray-200 text-gray-600 hover:bg-gray-200 hover:text-gray-600' - : '' + showDetails ? ' text-indigo-600 hover:text-indigo-700' : '' )} > - <DotsHorizontalIcon className="h-[24px] w-5" /> - </button> + <DotsHorizontalIcon className="h-5 w-5" /> + </IconButton> </Row> </Col> <div className="mt-2 mb-4 text-xs text-gray-500 md:text-sm"> diff --git a/web/components/share-icon-button.tsx b/web/components/share-icon-button.tsx index da1fc570..86a554f5 100644 --- a/web/components/share-icon-button.tsx +++ b/web/components/share-icon-button.tsx @@ -5,34 +5,22 @@ import clsx from 'clsx' import { copyToClipboard } from 'web/lib/util/copy' import { ToastClipboard } from 'web/components/toast-clipboard' import { track } from 'web/lib/service/analytics' -import { contractDetailsButtonClassName } from 'web/components/contract/contract-info-dialog' +import { IconButton } from './button' export function ShareIconButton(props: { - buttonClassName?: string - onCopyButtonClassName?: string toastClassName?: string children?: React.ReactNode iconClassName?: string copyPayload: string }) { - const { - buttonClassName, - onCopyButtonClassName, - toastClassName, - children, - iconClassName, - copyPayload, - } = props + const { toastClassName, children, iconClassName, copyPayload } = props const [showToast, setShowToast] = useState(false) return ( <div className="relative z-10 flex-shrink-0"> - <button - className={clsx( - contractDetailsButtonClassName, - buttonClassName, - showToast ? onCopyButtonClassName : '' - )} + <IconButton + size="2xs" + className={clsx('mt-1', showToast ? 'text-indigo-600' : '')} onClick={() => { copyToClipboard(copyPayload) track('copy share link') @@ -41,11 +29,11 @@ export function ShareIconButton(props: { }} > <LinkIcon - className={clsx(iconClassName ? iconClassName : 'h-[24px] w-5')} + className={clsx(iconClassName ? iconClassName : 'h-5 w-5')} aria-hidden="true" /> {children} - </button> + </IconButton> {showToast && <ToastClipboard className={toastClassName} />} </div> diff --git a/web/public/custom-components/tipJar.tsx b/web/public/custom-components/tipJar.tsx new file mode 100644 index 00000000..5682b779 --- /dev/null +++ b/web/public/custom-components/tipJar.tsx @@ -0,0 +1,23 @@ +export default function TipJar({ + size = 18, + color = '#66667C', + strokeWidth = 1.5, + fill = 'none', +}) { + return ( + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 18 18" + width={size} + height={size} + fill={fill} + stroke={color} + strokeWidth={strokeWidth} + opacity={50} + > + <path d="M15.5,8.1v5.8c0,1.43-1.16,2.6-2.6,2.6H5.1c-1.44,0-2.6-1.16-2.6-2.6v-5.8c0-1.04,.89-3.25,1.5-4.1h0v-2c0-.55,.45-1,1-1H13c.55,0,1,.45,1,1v2h0c.61,.85,1.5,3.06,1.5,4.1Z" /> + <line x1="4" y1="4" x2="9" y2="4" /> + <line x1="11.26" y1="4" x2="14" y2="4" /> + </svg> + ) +}