diff --git a/web/components/button.tsx b/web/components/button.tsx index cb39cba8..ea9a3e88 100644 --- a/web/components/button.tsx +++ b/web/components/button.tsx @@ -11,6 +11,7 @@ export type ColorType = | 'gray' | 'gradient' | 'gray-white' + | 'highlight-blue' export function Button(props: { className?: string @@ -56,7 +57,9 @@ export function Button(props: { color === 'gradient' && 'border-none bg-gradient-to-r from-indigo-500 to-blue-500 text-white hover:from-indigo-700 hover:to-blue-700', color === 'gray-white' && - 'border-none bg-white text-gray-500 shadow-none hover:bg-gray-200', + 'text-greyscale-6 hover:bg-greyscale-2 border-none shadow-none', + color === 'highlight-blue' && + 'text-highlight-blue border-none shadow-none', className )} disabled={disabled} diff --git a/web/components/contract/contract-details.tsx b/web/components/contract/contract-details.tsx index 0a65d4d9..d5b7b796 100644 --- a/web/components/contract/contract-details.tsx +++ b/web/components/contract/contract-details.tsx @@ -1,9 +1,4 @@ -import { - ClockIcon, - DatabaseIcon, - PencilIcon, - UserGroupIcon, -} from '@heroicons/react/outline' +import { ClockIcon } from '@heroicons/react/outline' import clsx from 'clsx' import { Editor } from '@tiptap/react' import dayjs from 'dayjs' @@ -16,9 +11,8 @@ import { DateTimeTooltip } from '../datetime-tooltip' import { fromNow } from 'web/lib/util/time' import { Avatar } from '../avatar' import { useState } from 'react' -import { ContractInfoDialog } from './contract-info-dialog' import NewContractBadge from '../new-contract-badge' -import { UserFollowButton } from '../follow-button' +import { MiniUserFollowButton } from '../follow-button' import { DAY_MS } from 'common/util/time' import { useUser } from 'web/hooks/use-user' import { exhibitExts } from 'common/util/parse' @@ -34,6 +28,9 @@ import { UserLink } from 'web/components/user-link' import { FeaturedContractBadge } from 'web/components/contract/featured-contract-badge' import { Tooltip } from 'web/components/tooltip' import { useWindowSize } from 'web/hooks/use-window-size' +import { ExtraContractActionsRow } from './extra-contract-actions-row' +import { PlusCircleIcon } from '@heroicons/react/solid' +import { GroupLink } from 'common/group' export type ShowTime = 'resolve-date' | 'close-date' @@ -111,90 +108,157 @@ export function AvatarDetails(props: { ) } +export function useIsMobile() { + const { width } = useWindowSize() + return (width ?? 0) < 600 +} + export function ContractDetails(props: { contract: Contract disabled?: boolean }) { const { contract, disabled } = props - const { - closeTime, - creatorName, - creatorUsername, - creatorId, - creatorAvatarUrl, - resolutionTime, - } = contract - const { volumeLabel, resolvedDate } = contractMetrics(contract) - const user = useUser() - const isCreator = user?.id === creatorId - const [open, setOpen] = useState(false) - const { width } = useWindowSize() - const isMobile = (width ?? 0) < 600 - const groupToDisplay = getGroupLinkToDisplay(contract) - const groupInfo = groupToDisplay ? ( - - - - {groupToDisplay.name} - - - ) : ( - - ) + const isMobile = useIsMobile() return ( - - - - {disabled ? ( - creatorName - ) : ( - - )} - {!disabled && } + + + +
+ +
- - {disabled ? ( - groupInfo - ) : !groupToDisplay && !user ? ( -
- ) : ( + {/* GROUPS */} + {isMobile && ( +
+ +
+ )} + + ) +} + +export function MarketSubheader(props: { + contract: Contract + disabled?: boolean +}) { + const { contract, disabled } = props + const { creatorName, creatorUsername, creatorId, creatorAvatarUrl } = contract + const { resolvedDate } = contractMetrics(contract) + const user = useUser() + const isCreator = user?.id === creatorId + const isMobile = useIsMobile() + return ( + + + {!disabled && ( +
+ +
+ )} + + + {disabled ? ( + creatorName + ) : ( + + )} + + + + {!isMobile && ( + + )} + + +
+ ) +} + +export function CloseOrResolveTime(props: { + contract: Contract + resolvedDate: any + isCreator: boolean +}) { + const { contract, resolvedDate, isCreator } = props + const { resolutionTime, closeTime } = contract + console.log(closeTime, resolvedDate) + if (!!closeTime || !!resolvedDate) { + return ( + + {resolvedDate && resolutionTime ? ( + <> + + +
resolved 
+ {resolvedDate} +
+
+ + ) : null} + + {!resolvedDate && closeTime && ( - {groupInfo} - {user && groupToDisplay && ( -
} + {!dayjs().isBefore(closeTime) &&
closed 
} + +
+ )} +
+ ) + } else return <> +} + +export function MarketGroups(props: { + contract: Contract + isMobile: boolean | undefined + disabled: boolean | undefined +}) { + const [open, setOpen] = useState(false) + const user = useUser() + const { contract, isMobile, disabled } = props + const groupToDisplay = getGroupLinkToDisplay(contract) + + return ( + <> + + + {!disabled && ( + + {user && ( + + + )} )} @@ -208,45 +272,7 @@ export function ContractDetails(props: { - - {(!!closeTime || !!resolvedDate) && ( - - {resolvedDate && resolutionTime ? ( - <> - - - {resolvedDate} - - - ) : null} - - {!resolvedDate && closeTime && user && ( - <> - - - - )} - - )} - {user && ( - <> - - -
{volumeLabel}
-
- {!disabled && ( - - )} - - )} -
+ ) } @@ -287,12 +313,12 @@ export function ExtraMobileContractDetails(props: { !resolvedDate && closeTime && ( + Closes  - Ends ) )} @@ -312,6 +338,45 @@ export function ExtraMobileContractDetails(props: { ) } +export function GroupDisplay(props: { + groupToDisplay?: GroupLink | null + isMobile?: boolean +}) { + const { groupToDisplay, isMobile } = props + if (groupToDisplay) { + return ( + + +
+ {groupToDisplay.name} +
+
+ + ) + } else + return ( + +
+ No Group +
+
+ ) +} + function EditableCloseDate(props: { closeTime: number contract: Contract diff --git a/web/components/contract/contract-info-dialog.tsx b/web/components/contract/contract-info-dialog.tsx index 76a48277..26c18484 100644 --- a/web/components/contract/contract-info-dialog.tsx +++ b/web/components/contract/contract-info-dialog.tsx @@ -19,6 +19,7 @@ import ShortToggle from '../widgets/short-toggle' import { DuplicateContractButton } from '../copy-contract-button' import { Row } from '../layout/row' import { BETTORS } from 'common/user' +import { Button } from '../button' import { capitalize } from 'lodash' export const contractDetailsButtonClassName = @@ -69,19 +70,21 @@ export function ContractInfoDialog(props: { return ( <> - + - + <Title className="!mt-0 !mb-0" text="This Market" /> <table className="table-compact table-zebra table w-full text-gray-500"> <tbody> diff --git a/web/components/contract/contract-overview.tsx b/web/components/contract/contract-overview.tsx index 1bfe84de..bfb4829f 100644 --- a/web/components/contract/contract-overview.tsx +++ b/web/components/contract/contract-overview.tsx @@ -25,11 +25,11 @@ import { NumericContract, PseudoNumericContract, } from 'common/contract' -import { ContractDetails, ExtraMobileContractDetails } from './contract-details' +import { ContractDetails } from './contract-details' import { NumericGraph } from './numeric-graph' const OverviewQuestion = (props: { text: string }) => ( - <Linkify className="text-2xl text-indigo-700 md:text-3xl" text={props.text} /> + <Linkify className="text-lg text-indigo-700 sm:text-2xl" text={props.text} /> ) const BetWidget = (props: { contract: CPMMContract }) => { @@ -73,7 +73,7 @@ const BinaryOverview = (props: { contract: BinaryContract; bets: Bet[] }) => { const { contract, bets } = props return ( <Col className="gap-1 md:gap-2"> - <Col className="gap-3 px-2 sm:gap-4"> + <Col className="gap-1 px-2"> <ContractDetails contract={contract} /> <Row className="justify-between gap-4"> <OverviewQuestion text={contract.question} /> @@ -85,7 +85,6 @@ const BinaryOverview = (props: { contract: BinaryContract; bets: Bet[] }) => { </Row> <Row className="items-center justify-between gap-4 xl:hidden"> <BinaryResolutionOrChance contract={contract} /> - <ExtraMobileContractDetails contract={contract} /> {tradingAllowed(contract) && ( <BetWidget contract={contract as CPMMBinaryContract} /> )} @@ -113,10 +112,6 @@ const ChoiceOverview = (props: { </Col> <Col className={'mb-1 gap-y-2'}> <AnswersGraph contract={contract} bets={[...bets].reverse()} /> - <ExtraMobileContractDetails - contract={contract} - forceShowVolume={true} - /> </Col> </Col> ) @@ -140,7 +135,6 @@ const PseudoNumericOverview = (props: { </Row> <Row className="items-center justify-between gap-4 xl:hidden"> <PseudoNumericResolutionOrExpectation contract={contract} /> - <ExtraMobileContractDetails contract={contract} /> {tradingAllowed(contract) && <BetWidget contract={contract} />} </Row> </Col> diff --git a/web/components/contract/extra-contract-actions-row.tsx b/web/components/contract/extra-contract-actions-row.tsx index 5d5ee4d8..af5db9c3 100644 --- a/web/components/contract/extra-contract-actions-row.tsx +++ b/web/components/contract/extra-contract-actions-row.tsx @@ -11,38 +11,29 @@ import { FollowMarketButton } from 'web/components/follow-market-button' import { LikeMarketButton } from 'web/components/contract/like-market-button' import { ContractInfoDialog } from 'web/components/contract/contract-info-dialog' import { Col } from 'web/components/layout/col' -import { withTracking } from 'web/lib/service/analytics' -import { CreateChallengeModal } from 'web/components/challenges/create-challenge-modal' -import { CHALLENGES_ENABLED } from 'common/challenge' -import ChallengeIcon from 'web/lib/icons/challenge-icon' export function ExtraContractActionsRow(props: { contract: Contract }) { const { contract } = props - const { outcomeType, resolution } = contract const user = useUser() const [isShareOpen, setShareOpen] = useState(false) - const [openCreateChallengeModal, setOpenCreateChallengeModal] = - useState(false) - const showChallenge = - user && outcomeType === 'BINARY' && !resolution && CHALLENGES_ENABLED return ( - <Row className={'mt-0.5 justify-around sm:mt-2 lg:justify-start'}> + <Row> + <FollowMarketButton contract={contract} user={user} /> + {user?.id !== contract.creatorId && ( + <LikeMarketButton contract={contract} user={user} /> + )} <Button - size="lg" + size="sm" color="gray-white" className={'flex'} onClick={() => { setShareOpen(true) }} > - <Col className={'items-center sm:flex-row'}> - <ShareIcon - className={clsx('h-[24px] w-5 sm:mr-2')} - aria-hidden="true" - /> - <span>Share</span> - </Col> + <Row> + <ShareIcon className={clsx('h-5 w-5')} aria-hidden="true" /> + </Row> <ShareModal isOpen={isShareOpen} setOpen={setShareOpen} @@ -50,35 +41,7 @@ export function ExtraContractActionsRow(props: { contract: Contract }) { user={user} /> </Button> - - {showChallenge && ( - <Button - size="lg" - color="gray-white" - className="max-w-xs self-center" - onClick={withTracking( - () => setOpenCreateChallengeModal(true), - 'click challenge button' - )} - > - <Col className="items-center sm:flex-row"> - <ChallengeIcon className="mx-auto h-[24px] w-5 text-gray-500 sm:mr-2" /> - <span>Challenge</span> - </Col> - <CreateChallengeModal - isOpen={openCreateChallengeModal} - setOpen={setOpenCreateChallengeModal} - user={user} - contract={contract} - /> - </Button> - )} - - <FollowMarketButton contract={contract} user={user} /> - {user?.id !== contract.creatorId && ( - <LikeMarketButton contract={contract} user={user} /> - )} - <Col className={'justify-center md:hidden'}> + <Col className={'justify-center'}> <ContractInfoDialog contract={contract} /> </Col> </Row> diff --git a/web/components/contract/like-market-button.tsx b/web/components/contract/like-market-button.tsx index e35e3e7e..01dce32f 100644 --- a/web/components/contract/like-market-button.tsx +++ b/web/components/contract/like-market-button.tsx @@ -38,15 +38,16 @@ export function LikeMarketButton(props: { return ( <Button - size={'lg'} + size={'sm'} className={'max-w-xs self-center'} color={'gray-white'} onClick={onLike} > - <Col className={'items-center sm:flex-row'}> + <Col className={'relative items-center sm:flex-row'}> <HeartIcon className={clsx( - 'h-[24px] w-5 sm:mr-2', + 'h-5 w-5 sm:h-6 sm:w-6', + totalTipped > 0 ? 'mr-2' : '', user && (userLikedContractIds?.includes(contract.id) || (!likes && contract.likedByUserIds?.includes(user.id))) @@ -54,7 +55,18 @@ export function LikeMarketButton(props: { : '' )} /> - Tip {totalTipped > 0 ? `(${formatMoney(totalTipped)})` : ''} + {totalTipped > 0 && ( + <div + className={clsx( + 'bg-greyscale-6 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', + totalTipped > 99 + ? 'text-[0.4rem] sm:text-[0.5rem]' + : 'sm:text-2xs text-[0.5rem]' + )} + > + {totalTipped} + </div> + )} </Col> </Button> ) diff --git a/web/components/follow-button.tsx b/web/components/follow-button.tsx index 09495169..6344757d 100644 --- a/web/components/follow-button.tsx +++ b/web/components/follow-button.tsx @@ -1,4 +1,6 @@ +import { CheckCircleIcon, PlusCircleIcon } from '@heroicons/react/solid' import clsx from 'clsx' +import { useEffect, useRef, useState } from 'react' import { useFollows } from 'web/hooks/use-follows' import { useUser } from 'web/hooks/use-user' import { follow, unfollow } from 'web/lib/firebase/users' @@ -54,18 +56,73 @@ export function FollowButton(props: { export function UserFollowButton(props: { userId: string; small?: boolean }) { const { userId, small } = props - const currentUser = useUser() - const following = useFollows(currentUser?.id) + const user = useUser() + const following = useFollows(user?.id) const isFollowing = following?.includes(userId) - if (!currentUser || currentUser.id === userId) return null + if (!user || user.id === userId) return null return ( <FollowButton isFollowing={isFollowing} - onFollow={() => follow(currentUser.id, userId)} - onUnfollow={() => unfollow(currentUser.id, userId)} + onFollow={() => follow(user.id, userId)} + onUnfollow={() => unfollow(user.id, userId)} small={small} /> ) } + +export function MiniUserFollowButton(props: { userId: string }) { + const { userId } = props + const user = useUser() + const following = useFollows(user?.id) + const isFollowing = following?.includes(userId) + const isFirstRender = useRef(true) + const [justFollowed, setJustFollowed] = useState(false) + + useEffect(() => { + if (isFirstRender.current) { + if (isFollowing != undefined) { + isFirstRender.current = false + } + return + } + if (isFollowing) { + setJustFollowed(true) + setTimeout(() => { + setJustFollowed(false) + }, 1000) + } + }, [isFollowing]) + + if (justFollowed) { + return ( + <CheckCircleIcon + className={clsx( + 'text-highlight-blue ml-3 mt-2 h-5 w-5 rounded-full bg-white sm:mr-2' + )} + aria-hidden="true" + /> + ) + } + if ( + !user || + user.id === userId || + isFollowing || + !user || + isFollowing === undefined + ) + return null + return ( + <> + <button onClick={withTracking(() => follow(user.id, userId), 'follow')}> + <PlusCircleIcon + className={clsx( + 'text-highlight-blue hover:text-hover-blue mt-2 ml-3 h-5 w-5 rounded-full bg-white sm:mr-2' + )} + aria-hidden="true" + /> + </button> + </> + ) +} diff --git a/web/components/follow-market-button.tsx b/web/components/follow-market-button.tsx index 1dd261cb..0e65165b 100644 --- a/web/components/follow-market-button.tsx +++ b/web/components/follow-market-button.tsx @@ -25,7 +25,7 @@ export const FollowMarketButton = (props: { return ( <Button - size={'lg'} + size={'sm'} color={'gray-white'} onClick={async () => { if (!user) return firebaseLogin() @@ -56,13 +56,19 @@ export const FollowMarketButton = (props: { > {followers?.includes(user?.id ?? 'nope') ? ( <Col className={'items-center gap-x-2 sm:flex-row'}> - <EyeOffIcon className={clsx('h-6 w-6')} aria-hidden="true" /> - Unwatch + <EyeOffIcon + className={clsx('h-5 w-5 sm:h-6 sm:w-6')} + aria-hidden="true" + /> + {/* Unwatch */} </Col> ) : ( <Col className={'items-center gap-x-2 sm:flex-row'}> - <EyeIcon className={clsx('h-6 w-6')} aria-hidden="true" /> - Watch + <EyeIcon + className={clsx('h-5 w-5 sm:h-6 sm:w-6')} + aria-hidden="true" + /> + {/* Watch */} </Col> )} <WatchMarketModal diff --git a/web/pages/[username]/[contractSlug].tsx b/web/pages/[username]/[contractSlug].tsx index 2c011c90..a0b2ed50 100644 --- a/web/pages/[username]/[contractSlug].tsx +++ b/web/pages/[username]/[contractSlug].tsx @@ -37,7 +37,6 @@ import { User } from 'common/user' import { ContractComment } from 'common/comment' import { getOpenGraphProps } from 'common/contract-details' import { ContractDescription } from 'web/components/contract/contract-description' -import { ExtraContractActionsRow } from 'web/components/contract/extra-contract-actions-row' import { ContractLeaderboard, ContractTopTrades, @@ -257,7 +256,6 @@ export function ContractPageContent( )} <ContractOverview contract={contract} bets={nonChallengeBets} /> - <ExtraContractActionsRow contract={contract} /> <ContractDescription className="mb-6 px-2" contract={contract} /> {outcomeType === 'NUMERIC' && ( diff --git a/web/pages/embed/[username]/[contractSlug].tsx b/web/pages/embed/[username]/[contractSlug].tsx index fbeef88f..62dd1ae1 100644 --- a/web/pages/embed/[username]/[contractSlug].tsx +++ b/web/pages/embed/[username]/[contractSlug].tsx @@ -11,7 +11,7 @@ import { NumericResolutionOrExpectation, PseudoNumericResolutionOrExpectation, } from 'web/components/contract/contract-card' -import { ContractDetails } from 'web/components/contract/contract-details' +import { MarketSubheader } from 'web/components/contract/contract-details' import { ContractProbGraph } from 'web/components/contract/contract-prob-graph' import { NumericGraph } from 'web/components/contract/numeric-graph' import { Col } from 'web/components/layout/col' @@ -102,50 +102,40 @@ export function ContractEmbed(props: { contract: Contract; bets: Bet[] }) { return ( <Col className="h-[100vh] w-full bg-white"> - <div className="relative flex flex-col pt-2"> - <div className="px-3 text-xl text-indigo-700 md:text-2xl"> + <Row className="justify-between gap-4 px-2"> + <div className="text-xl text-indigo-700 md:text-2xl"> <SiteLink href={href}>{question}</SiteLink> </div> + {isBinary && ( + <BinaryResolutionOrChance contract={contract} probAfter={probAfter} /> + )} - <Spacer h={3} /> + {isPseudoNumeric && ( + <PseudoNumericResolutionOrExpectation contract={contract} /> + )} - <Row className="items-center justify-between gap-4 px-2"> - <ContractDetails contract={contract} disabled /> + {outcomeType === 'FREE_RESPONSE' && ( + <FreeResponseResolutionOrChance contract={contract} truncate="long" /> + )} - {(isBinary || isPseudoNumeric) && - tradingAllowed(contract) && - !betPanelOpen && ( - <Button color="gradient" onClick={() => setBetPanelOpen(true)}> - Predict - </Button> - )} + {outcomeType === 'NUMERIC' && ( + <NumericResolutionOrExpectation contract={contract} /> + )} + </Row> + <Spacer h={3} /> + <Row className="items-center justify-between gap-4 px-2"> + <MarketSubheader contract={contract} disabled /> - {isBinary && ( - <BinaryResolutionOrChance - contract={contract} - probAfter={probAfter} - className="items-center" - /> + {(isBinary || isPseudoNumeric) && + tradingAllowed(contract) && + !betPanelOpen && ( + <Button color="gradient" onClick={() => setBetPanelOpen(true)}> + Predict + </Button> )} + </Row> - {isPseudoNumeric && ( - <PseudoNumericResolutionOrExpectation contract={contract} /> - )} - - {outcomeType === 'FREE_RESPONSE' && ( - <FreeResponseResolutionOrChance - contract={contract} - truncate="long" - /> - )} - - {outcomeType === 'NUMERIC' && ( - <NumericResolutionOrExpectation contract={contract} /> - )} - </Row> - - <Spacer h={2} /> - </div> + <Spacer h={2} /> {(isBinary || isPseudoNumeric) && betPanelOpen && ( <BetInline diff --git a/web/tailwind.config.js b/web/tailwind.config.js index eb411216..7bea3ec2 100644 --- a/web/tailwind.config.js +++ b/web/tailwind.config.js @@ -26,6 +26,8 @@ module.exports = { 'greyscale-5': '#9191A7', 'greyscale-6': '#66667C', 'greyscale-7': '#111140', + 'highlight-blue': '#5BCEFF', + 'hover-blue': '#90DEFF', }, typography: { quoteless: {