From 1e66f4d1402618b641d8fbefc3d01d9f16c0cee5 Mon Sep 17 00:00:00 2001 From: mantikoros <95266179+mantikoros@users.noreply.github.com> Date: Fri, 5 Aug 2022 00:22:45 -0500 Subject: [PATCH] Share row (#715) * Challenge bets * Store avatar url * Fix before and after probs * Check balance before creation * Calculate winning shares * pretty * Change winning value * Set shares to equal each other * Fix share challenge link * pretty * remove lib refs * Probability of bet is set to market * Remove peer pill * Cleanup * Button on contract page * don't show challenge if not binary or if resolved * challenge button (WIP) * fix accept challenge: don't change pool/probability * Opengraph preview [WIP] * elim lib * Edit og card props * Change challenge text * New card gen attempt * Get challenge on server * challenge button styling * Use env domain * Remove other window ref * Use challenge creator as avatar * Remove user name * Remove s from property, replace prob with outcome * challenge form * share text * Add in challenge parts to template and url * Challenge url params optional * Add challenge params to parse request * Parse please * Don't remove prob * Challenge card styling * Challenge card styling * Challenge card styling * Challenge card styling * Challenge card styling * Challenge card styling * Challenge card styling * Challenge card styling * Add to readme about how to dev og-image * Add emojis * button: gradient background, 2xl size * beautify accept bet screen * update question button * Add separate challenge template * Accepted challenge sharing card, fix accept bet call * accept challenge button * challenge winner page * create challenge screen * Your outcome/cost=> acceptorOutcome/cost * New create challenge panel * Fix main merge * Add challenge slug to bet and filter by it * Center title * Add helper text * Add FAQ section * Lint * Columnize the user areas in preview link too * Absolutely position * Spacing * Orientation * Restyle challenges list, cache contract name * Make copying easy on mobile * Link spacing * Fix spacing * qr codes! * put your challenges first * eslint * Changes to contract buttons and create challenge modal * Change titles around for current bet * Add back in contract title after winning * Cleanup * Add challenge enabled flag * Spacing of switch button * market share row * Add lite market endpoint * 500 mana email (#687) * Create 500-mana.html * Update 500-mana.html Fixed typos and links not working * Added "create a good market" guide added page creating-market.html For Stephen to set up condition (email 3 days after signing up) * Update 500-mana.html updated 500 Mana email (still need to make changes to create market guide) * email changes * sendOneWeekBonusEmail logic * add dayjs as dependency * don't use mailgun scheduling Co-authored-by: mantikoros * Challenge Bets (#679) * Challenge bets * Store avatar url * Fix before and after probs * Check balance before creation * Calculate winning shares * pretty * Change winning value * Set shares to equal each other * Fix share challenge link * pretty * remove lib refs * Probability of bet is set to market * Remove peer pill * Cleanup * Button on contract page * don't show challenge if not binary or if resolved * challenge button (WIP) * fix accept challenge: don't change pool/probability * Opengraph preview [WIP] * elim lib * Edit og card props * Change challenge text * New card gen attempt * Get challenge on server * challenge button styling * Use env domain * Remove other window ref * Use challenge creator as avatar * Remove user name * Remove s from property, replace prob with outcome * challenge form * share text * Add in challenge parts to template and url * Challenge url params optional * Add challenge params to parse request * Parse please * Don't remove prob * Challenge card styling * Challenge card styling * Challenge card styling * Challenge card styling * Challenge card styling * Challenge card styling * Challenge card styling * Challenge card styling * Add to readme about how to dev og-image * Add emojis * button: gradient background, 2xl size * beautify accept bet screen * update question button * Add separate challenge template * Accepted challenge sharing card, fix accept bet call * accept challenge button * challenge winner page * create challenge screen * Your outcome/cost=> acceptorOutcome/cost * New create challenge panel * Fix main merge * Add challenge slug to bet and filter by it * Center title * Add helper text * Add FAQ section * Lint * Columnize the user areas in preview link too * Absolutely position * Spacing * Orientation * Restyle challenges list, cache contract name * Make copying easy on mobile * Link spacing * Fix spacing * qr codes! * put your challenges first * eslint * Changes to contract buttons and create challenge modal * Change titles around for current bet * Add back in contract title after winning * Cleanup * Add challenge enabled flag * Spacing of switch button * Put sharing qr code in modal Co-authored-by: mantikoros * See challenges you've accepted too * Remove max height * Notify mentioned users on market publish (#683) * Add function to parse at mentions * Notify mentioned users on market create - refactor createNotification to accept list of recipients' ids * Switch comments/chat to rich text editor (#703) * Switch comments/chat to rich text editor * Remove TruncatedComment * Re-add submit on enter * Insert at mention on reply * Update editor style for send button * only submit on enter in chat * code review: refactor * use more specific type for upload * fix ESlint and errors from merge * fix trigger on every render eslint warning * Notify people mentioned in comment * fix type errors * Revert "Switch comments/chat to rich text editor (#703)" This reverts commit f52da72115bfacb0af5a4d54c137a936b33d9eee. * merge conflict * share modal * merge issue * eslint * bigger link icion Co-authored-by: Ian Philips Co-authored-by: James Grugett Co-authored-by: SirSaltyy <104849031+SirSaltyy@users.noreply.github.com> Co-authored-by: Sinclair Chen --- web/components/button.tsx | 2 +- .../challenges/create-challenge-modal.tsx | 248 ++++++++++++++++++ web/components/contract/contract-details.tsx | 12 +- .../contract/contract-info-dialog.tsx | 31 +-- web/components/contract/contract-overview.tsx | 52 +--- web/components/contract/share-modal.tsx | 77 ++++++ web/components/contract/share-row.tsx | 59 +++++ web/pages/challenges/index.tsx | 1 + 8 files changed, 394 insertions(+), 88 deletions(-) create mode 100644 web/components/challenges/create-challenge-modal.tsx create mode 100644 web/components/contract/share-modal.tsx create mode 100644 web/components/contract/share-row.tsx 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) + }} + > + + <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-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: { <div className="whitespace-nowrap">{volumeLabel}</div> </Row> - <ShareIconButton - copyPayload={`https://${ENV_CONFIG.domain}${contractPath(contract)}${ - user?.username && contract.creatorUsername !== user?.username - ? '?referrer=' + user?.username - : '' - }`} - toastClassName={'sm:-left-40 -left-24 min-w-[250%]'} - /> {!disabled && <ContractInfoDialog contract={contract} bets={bets} />} </Row> 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[] }) { <Col className="gap-4 rounded bg-white p-6"> <Title className="!mt-0 !mb-0" text="Market info" /> - <div>Share</div> - - <Row className="justify-start gap-4"> - <TweetButton - className="self-start" - tweetText={getTweetText(contract)} - /> - <ShareEmbedButton contract={contract} toastClassName={'-left-20'} /> - <DuplicateContractButton contract={contract} /> - </Row> - <div /> - - <div>Stats</div> - <table className="table-compact table-zebra table w-full text-gray-500"> <tbody> <tr> @@ -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 ( <Col className={clsx('mb-6', className)}> @@ -123,47 +118,12 @@ export const ContractOverview = (props: { <AnswersGraph contract={contract} bets={bets} /> )} {outcomeType === 'NUMERIC' && <NumericGraph contract={contract} />} - {/* {(contract.description || isCreator) && <Spacer h={6} />} */} + <ShareRow user={user} contract={contract} /> <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/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 = <LinkIcon className="mr-2 h-6 w-6" aria-hidden="true" /> + + const copyPayload = `https://${ENV_CONFIG.domain}${contractPath(contract)}${ + user?.username && contract.creatorUsername !== user?.username + ? '?referrer=' + user?.username + : '' + }` + + return ( + <Modal open={isOpen} setOpen={setOpen}> + <Col className="gap-4 rounded bg-white p-4"> + <Title className="!mt-0 mb-2" text="Share this market" /> + + <Button + size="2xl" + color="gradient" + className={'mb-2 flex max-w-xs self-center'} + onClick={() => { + copyToClipboard(copyPayload) + track('copy share link') + toast.success('Link copied!', { + icon: linkIcon, + }) + }} + > + {linkIcon} Copy link + </Button> + + <Row className="justify-start gap-4 self-center"> + <TweetButton + className="self-start" + tweetText={getTweetText(contract)} + /> + <ShareEmbedButton contract={contract} toastClassName={'-left-20'} /> + <DuplicateContractButton contract={contract} /> + </Row> + </Col> + </Modal> + ) +} + +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 ( + <Row className="mt-2"> + <Button + size="lg" + color="gray-white" + className={'flex'} + onClick={() => { + setShareOpen(true) + }} + > + <ShareIcon className={clsx('mr-2 h-[24px] w-5')} aria-hidden="true" /> + Share + <ShareModal + isOpen={isShareOpen} + setOpen={setShareOpen} + contract={contract} + user={user} + /> + </Button> + + {showChallenge && ( + <Button size="lg" color="gray-white" onClick={() => setIsOpen(true)}> + ⚔️ Challenge + <CreateChallengeModal + isOpen={isOpen} + setOpen={setIsOpen} + user={user} + contract={contract} + /> + </Button> + )} + </Row> + ) +} 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'