diff --git a/docs/docs/faq.md b/docs/docs/faq.md index 01c4dc36..5c369e39 100644 --- a/docs/docs/faq.md +++ b/docs/docs/faq.md @@ -4,11 +4,7 @@ ### Do I have to pay real money in order to participate? -Nope! Each account starts with a free M$1000. If you invest it wisely, you can increase your total without ever needing to put any real money into the site. - -### What is the name for the currency Manifold uses, represented by M$? - -Manifold Dollars, or mana for short. +Nope! Each account starts with a free 1000 mana (or M$1000 for short). If you invest it wisely, you can increase your total without ever needing to put any real money into the site. ### Can M$ be sold for real money? diff --git a/web/components/add-funds-button.tsx b/web/components/add-funds-button.tsx index 90b24b2c..b610bfee 100644 --- a/web/components/add-funds-button.tsx +++ b/web/components/add-funds-button.tsx @@ -30,10 +30,10 @@ export function AddFundsButton(props: { className?: string }) {
-
Get Manifold Dollars
+
Get Mana
- Use Manifold Dollars to trade in your favorite markets.
(Not + Buy mana (M$) to trade in your favorite markets.
(Not redeemable for cash.)
diff --git a/web/components/answers/answers-panel.tsx b/web/components/answers/answers-panel.tsx index 41b7f0f9..51cf5799 100644 --- a/web/components/answers/answers-panel.tsx +++ b/web/components/answers/answers-panel.tsx @@ -1,4 +1,4 @@ -import { sortBy, partition, sum, uniq } from 'lodash' +import { sortBy, partition, sum } from 'lodash' import { useEffect, useState } from 'react' import { FreeResponseContract, MultipleChoiceContract } from 'common/contract' @@ -11,7 +11,6 @@ import { AnswerItem } from './answer-item' import { CreateAnswerPanel } from './create-answer-panel' import { AnswerResolvePanel } from './answer-resolve-panel' import { Spacer } from '../layout/spacer' -import { User } from 'common/user' import { getOutcomeProbability } from 'common/calculate' import { Answer } from 'common/answer' import clsx from 'clsx' @@ -39,22 +38,14 @@ export function AnswersPanel(props: { const answers = (useAnswers(contract.id) ?? contract.answers).filter( (a) => a.number != 0 || contract.outcomeType === 'MULTIPLE_CHOICE' ) - const hasZeroBetAnswers = answers.some((answer) => totalBets[answer.id] < 1) - - const [winningAnswers, losingAnswers] = partition( - answers.filter((a) => (showAllAnswers ? true : totalBets[a.id] > 0)), - (answer) => - answer.id === resolution || (resolutions && resolutions[answer.id]) + const [winningAnswers, notWinningAnswers] = partition( + answers, + (a) => a.id === resolution || (resolutions && resolutions[a.id]) + ) + const [visibleAnswers, invisibleAnswers] = partition( + sortBy(notWinningAnswers, (a) => -getOutcomeProbability(contract, a.id)), + (a) => showAllAnswers || totalBets[a.id] > 0 ) - const sortedAnswers = [ - ...sortBy(winningAnswers, (answer) => - resolutions ? -1 * resolutions[answer.id] : 0 - ), - ...sortBy( - resolution ? [] : losingAnswers, - (answer) => -1 * getDpmOutcomeProbability(contract.totalShares, answer.id) - ), - ] const user = useUser() @@ -67,12 +58,6 @@ export function AnswersPanel(props: { const chosenTotal = sum(Object.values(chosenAnswers)) - const answerItems = getAnswerItems( - contract, - losingAnswers.length > 0 ? losingAnswers : sortedAnswers, - user - ) - const onChoose = (answerId: string, prob: number) => { if (resolveOption === 'CHOOSE') { setChosenAnswers({ [answerId]: prob }) @@ -109,13 +94,13 @@ export function AnswersPanel(props: { return ( {(resolveOption || resolution) && - sortedAnswers.map((answer) => ( + sortBy(winningAnswers, (a) => -(resolutions?.[a.id] ?? 0)).map((a) => ( -
- {answerItems.map((item) => ( -
-
- -
-
- ))} - - {hasZeroBetAnswers && !showAllAnswers && ( - - )} - -
-
+ + {visibleAnswers.map((a) => ( + + ))} + {invisibleAnswers.length > 0 && !showAllAnswers && ( + + )} + )} - {answers.length <= 1 && ( + {answers.length === 0 && (
No answers yet...
)} @@ -175,35 +158,9 @@ export function AnswersPanel(props: { ) } -function getAnswerItems( - contract: FreeResponseContract | MultipleChoiceContract, - answers: Answer[], - user: User | undefined | null -) { - let outcomes = uniq(answers.map((answer) => answer.number.toString())) - outcomes = sortBy(outcomes, (outcome) => - getOutcomeProbability(contract, outcome) - ).reverse() - - return outcomes - .map((outcome) => { - const answer = answers.find((answer) => answer.id === outcome) as Answer - //unnecessary - return { - id: outcome, - type: 'answer' as const, - contract, - answer, - user, - } - }) - .filter((group) => group.answer) -} - function OpenAnswer(props: { contract: FreeResponseContract | MultipleChoiceContract answer: Answer - type: string }) { const { answer, contract } = props const { username, avatarUrl, name, text } = answer @@ -212,7 +169,7 @@ function OpenAnswer(props: { const [open, setOpen] = useState(false) return ( - + -
- -
+
answered
- - - - - -
- - {probPercent} - - setOpen(true)} - /> -
+ + + + {probPercent} + + setOpen(true)} + /> diff --git a/web/components/auth-context.tsx b/web/components/auth-context.tsx index d7c7b717..19ced0b2 100644 --- a/web/components/auth-context.tsx +++ b/web/components/auth-context.tsx @@ -17,7 +17,7 @@ import { setCookie } from 'web/lib/util/cookie' // Either we haven't looked up the logged in user yet (undefined), or we know // the user is not logged in (null), or we know the user is logged in. -type AuthUser = undefined | null | UserAndPrivateUser +export type AuthUser = undefined | null | UserAndPrivateUser const TEN_YEARS_SECS = 60 * 60 * 24 * 365 * 10 const CACHED_USER_KEY = 'CACHED_USER_KEY_V2' diff --git a/web/components/avatar.tsx b/web/components/avatar.tsx index 44c37128..abb67d46 100644 --- a/web/components/avatar.tsx +++ b/web/components/avatar.tsx @@ -40,7 +40,7 @@ export function Avatar(props: { style={{ maxWidth: `${s * 0.25}rem` }} src={avatarUrl} onClick={onClick} - alt={username} + alt={`${username ?? 'Unknown user'} avatar`} onError={() => { // If the image doesn't load, clear the avatarUrl to show the default // Mostly for localhost, when getting a 403 from googleusercontent diff --git a/web/components/comment-input.tsx b/web/components/comment-input.tsx index bf3730f3..3ba6f2ce 100644 --- a/web/components/comment-input.tsx +++ b/web/components/comment-input.tsx @@ -11,7 +11,7 @@ import { Row } from './layout/row' import { LoadingIndicator } from './loading-indicator' export function CommentInput(props: { - replyToUser?: { id: string; username: string } + replyTo?: { id: string; username: string } // Reply to a free response answer parentAnswerOutcome?: string // Reply to another comment @@ -19,7 +19,7 @@ export function CommentInput(props: { onSubmitComment?: (editor: Editor) => void className?: string }) { - const { parentAnswerOutcome, parentCommentId, replyToUser, onSubmitComment } = + const { parentAnswerOutcome, parentCommentId, replyTo, onSubmitComment } = props const user = useUser() @@ -55,7 +55,7 @@ export function CommentInput(props: { [0]['upload'] submitComment: () => void isSubmitting: boolean }) { - const { user, editor, upload, submitComment, isSubmitting, replyToUser } = - props + const { user, editor, upload, submitComment, isSubmitting, replyTo } = props useEffect(() => { editor?.setEditable(!isSubmitting) }, [isSubmitting, editor]) @@ -108,12 +107,12 @@ export function CommentInputTextArea(props: { }, }) // insert at mention and focus - if (replyToUser) { + if (replyTo) { editor .chain() .setContent({ type: 'mention', - attrs: { label: replyToUser.username, id: replyToUser.id }, + attrs: { label: replyTo.username, id: replyTo.id }, }) .insertContent(' ') .focus() @@ -127,7 +126,7 @@ export function CommentInputTextArea(props: { {user && !isSubmitting && ( + + + + {children} + </Col> + </Modal> + </> + ) +} diff --git a/web/components/contract/contract-leaderboard.tsx b/web/components/contract/contract-leaderboard.tsx index 4d25ffa4..f984e3b6 100644 --- a/web/components/contract/contract-leaderboard.tsx +++ b/web/components/contract/contract-leaderboard.tsx @@ -1,11 +1,10 @@ import { Bet } from 'common/bet' -import { ContractComment } from 'common/comment' import { resolvedPayout } from 'common/calculate' import { Contract } from 'common/contract' import { formatMoney } from 'common/util/format' import { groupBy, mapValues, sumBy, sortBy, keyBy } from 'lodash' -import { useState, useMemo, useEffect } from 'react' -import { listUsers, User } from 'web/lib/firebase/users' +import { memo } from 'react' +import { useComments } from 'web/hooks/use-comments' import { FeedBet } from '../feed/feed-bets' import { FeedComment } from '../feed/feed-comments' import { Spacer } from '../layout/spacer' @@ -13,61 +12,48 @@ import { Leaderboard } from '../leaderboard' import { Title } from '../title' import { BETTORS } from 'common/user' -export function ContractLeaderboard(props: { +export const ContractLeaderboard = memo(function ContractLeaderboard(props: { contract: Contract bets: Bet[] }) { const { contract, bets } = props - const [users, setUsers] = useState<User[]>() - const { userProfits, top5Ids } = useMemo(() => { - // Create a map of userIds to total profits (including sales) - const openBets = bets.filter((bet) => !bet.isSold && !bet.sale) - const betsByUser = groupBy(openBets, 'userId') - - const userProfits = mapValues(betsByUser, (bets) => - sumBy(bets, (bet) => resolvedPayout(contract, bet) - bet.amount) - ) - // Find the 5 users with the most profits - const top5Ids = Object.entries(userProfits) - .sort(([_i1, p1], [_i2, p2]) => p2 - p1) - .filter(([, p]) => p > 0) - .slice(0, 5) - .map(([id]) => id) - return { userProfits, top5Ids } - }, [contract, bets]) - - useEffect(() => { - if (top5Ids.length > 0) { - listUsers(top5Ids).then((users) => { - const sortedUsers = sortBy(users, (user) => -userProfits[user.id]) - setUsers(sortedUsers) - }) + // Create a map of userIds to total profits (including sales) + const openBets = bets.filter((bet) => !bet.isSold && !bet.sale) + const betsByUser = groupBy(openBets, 'userId') + const userProfits = mapValues(betsByUser, (bets) => { + return { + name: bets[0].userName, + username: bets[0].userUsername, + avatarUrl: bets[0].userAvatarUrl, + total: sumBy(bets, (bet) => resolvedPayout(contract, bet) - bet.amount), } - }, [userProfits, top5Ids]) + }) + // Find the 5 users with the most profits + const top5 = Object.values(userProfits) + .sort((p1, p2) => p2.total - p1.total) + .filter((p) => p.total > 0) + .slice(0, 5) - return users && users.length > 0 ? ( + return top5 && top5.length > 0 ? ( <Leaderboard title={`🏅 Top ${BETTORS}`} - users={users || []} + entries={top5 || []} columns={[ { header: 'Total profit', - renderCell: (user) => formatMoney(userProfits[user.id] || 0), + renderCell: (entry) => formatMoney(entry.total), }, ]} className="mt-12 max-w-sm" /> ) : null -} +}) -export function ContractTopTrades(props: { - contract: Contract - bets: Bet[] - comments: ContractComment[] -}) { - const { contract, bets, comments } = props - const commentsById = keyBy(comments, 'id') +export function ContractTopTrades(props: { contract: Contract; bets: Bet[] }) { + const { contract, bets } = props + // todo: this stuff should be calced in DB at resolve time + const comments = useComments(contract.id) const betsById = keyBy(bets, 'id') // If 'id2' is the sale of 'id1', both are logged with (id2 - id1) of profit @@ -88,29 +74,23 @@ export function ContractTopTrades(props: { const topBetId = sortBy(bets, (b) => -profitById[b.id])[0]?.id const topBettor = betsById[topBetId]?.userName - // And also the commentId of the comment with the highest profit - const topCommentId = sortBy( - comments, - (c) => c.betId && -profitById[c.betId] - )[0]?.id + // And also the comment with the highest profit + const topComment = sortBy(comments, (c) => c.betId && -profitById[c.betId])[0] return ( <div className="mt-12 max-w-sm"> - {topCommentId && profitById[topCommentId] > 0 && ( + {topComment && profitById[topComment.id] > 0 && ( <> <Title text="💬 Proven correct" className="!mt-0" /> <div className="relative flex items-start space-x-3 rounded-md bg-gray-50 px-2 py-4"> - <FeedComment - contract={contract} - comment={commentsById[topCommentId]} - /> + <FeedComment contract={contract} comment={topComment} /> </div> <Spacer h={16} /> </> )} {/* If they're the same, only show the comment; otherwise show both */} - {topBettor && topBetId !== topCommentId && profitById[topBetId] > 0 && ( + {topBettor && topBetId !== topComment?.betId && profitById[topBetId] > 0 && ( <> <Title text="💸 Best bet" className="!mt-0" /> <div className="relative flex items-start space-x-3 rounded-md bg-gray-50 px-2 py-4"> diff --git a/web/components/contract/contract-prob-graph.tsx b/web/components/contract/contract-prob-graph.tsx index aad44b82..60ef85b5 100644 --- a/web/components/contract/contract-prob-graph.tsx +++ b/web/components/contract/contract-prob-graph.tsx @@ -47,14 +47,14 @@ export const ContractProbGraph = memo(function ContractProbGraph(props: { times.push(latestTime.valueOf()) probs.push(probs[probs.length - 1]) - const quartiles = [0, 25, 50, 75, 100] + const { width } = useWindowSize() + + const quartiles = !width || width < 800 ? [0, 50, 100] : [0, 25, 50, 75, 100] const yTickValues = isBinary ? quartiles : quartiles.map((x) => x / 100).map(f) - const { width } = useWindowSize() - const numXTickValues = !width || width < 800 ? 2 : 5 const startDate = dayjs(times[0]) const endDate = startDate.add(1, 'hour').isAfter(latestTime) @@ -104,7 +104,7 @@ export const ContractProbGraph = memo(function ContractProbGraph(props: { return ( <div className="w-full overflow-visible" - style={{ height: height ?? (!width || width >= 800 ? 350 : 250) }} + style={{ height: height ?? (!width || width >= 800 ? 250 : 150) }} > <ResponsiveLine data={data} @@ -144,7 +144,7 @@ export const ContractProbGraph = memo(function ContractProbGraph(props: { pointBorderWidth={1} pointBorderColor="#fff" enableSlices="x" - enableGridX={!!width && width >= 800} + enableGridX={false} enableArea areaBaselineValue={isBinary || isLogScale ? 0 : contract.min} margin={{ top: 20, right: 20, bottom: 25, left: 40 }} diff --git a/web/components/contract/contract-tabs.tsx b/web/components/contract/contract-tabs.tsx index 245a8d7d..17471796 100644 --- a/web/components/contract/contract-tabs.tsx +++ b/web/components/contract/contract-tabs.tsx @@ -5,19 +5,19 @@ import { FeedBet } from '../feed/feed-bets' import { FeedLiquidity } from '../feed/feed-liquidity' import { FeedAnswerCommentGroup } from '../feed/feed-answer-comment-group' import { FeedCommentThread, ContractCommentInput } from '../feed/feed-comments' -import { CommentTipMap } from 'web/hooks/use-tip-txns' import { groupBy, sortBy } from 'lodash' import { Bet } from 'common/bet' -import { Contract, FreeResponseContract } from 'common/contract' -import { ContractComment } from 'common/comment' -import { PAST_BETS, User } from 'common/user' +import { Contract } from 'common/contract' +import { PAST_BETS } from 'common/user' import { ContractBetsTable, BetsSummary } from '../bets-list' import { Spacer } from '../layout/spacer' import { Tabs } from '../layout/tabs' import { Col } from '../layout/col' +import { LoadingIndicator } from 'web/components/loading-indicator' import { useComments } from 'web/hooks/use-comments' import { useLiquidity } from 'web/hooks/use-liquidity' import { useTipTxns } from 'web/hooks/use-tip-txns' +import { useUser } from 'web/hooks/use-user' import { capitalize } from 'lodash' import { DEV_HOUSE_LIQUIDITY_PROVIDER_ID, @@ -25,21 +25,13 @@ import { } from 'common/antes' import { useIsMobile } from 'web/hooks/use-is-mobile' -export function ContractTabs(props: { - contract: Contract - user: User | null | undefined - bets: Bet[] - comments: ContractComment[] -}) { - const { contract, user, bets, comments } = props +export function ContractTabs(props: { contract: Contract; bets: Bet[] }) { + const { contract, bets } = props const isMobile = useIsMobile() - + const user = useUser() const userBets = user && bets.filter((bet) => !bet.isAnte && bet.userId === user.id) - const visibleBets = bets.filter( - (bet) => !bet.isAnte && !bet.isRedemption && bet.amount !== 0 - ) const yourTrades = ( <div> @@ -57,19 +49,16 @@ export function ContractTabs(props: { return ( <Tabs + className="mb-4" currentPageForAnalytics={'contract'} tabs={[ { title: 'Comments', - content: ( - <CommentsTabContent contract={contract} comments={comments} /> - ), + content: <CommentsTabContent contract={contract} />, }, { title: capitalize(PAST_BETS), - content: ( - <ContractBetsActivity contract={contract} bets={visibleBets} /> - ), + content: <BetsTabContent contract={contract} bets={bets} />, }, ...(!user || !userBets?.length ? [] @@ -86,46 +75,87 @@ export function ContractTabs(props: { const CommentsTabContent = memo(function CommentsTabContent(props: { contract: Contract - comments: ContractComment[] }) { - const { contract, comments } = props + const { contract } = props const tips = useTipTxns({ contractId: contract.id }) - const updatedComments = useComments(contract.id) ?? comments + const comments = useComments(contract.id) + if (comments == null) { + return <LoadingIndicator /> + } if (contract.outcomeType === 'FREE_RESPONSE') { + const generalComments = comments.filter( + (c) => c.answerOutcome === undefined && c.betId === undefined + ) + const sortedAnswers = sortBy( + contract.answers, + (a) => -getOutcomeProbability(contract, a.id) + ) + const commentsByOutcome = groupBy( + comments, + (c) => c.answerOutcome ?? c.betOutcome ?? '_' + ) return ( <> - <FreeResponseContractCommentsActivity - contract={contract} - comments={updatedComments} - tips={tips} - /> + {sortedAnswers.map((answer) => ( + <div key={answer.id} className="relative pb-4"> + <span + className="absolute top-5 left-5 -ml-px h-[calc(100%-2rem)] w-0.5 bg-gray-200" + aria-hidden="true" + /> + <FeedAnswerCommentGroup + contract={contract} + answer={answer} + answerComments={sortBy( + commentsByOutcome[answer.number.toString()] ?? [], + (c) => c.createdTime + )} + tips={tips} + /> + </div> + ))} <Col className="mt-8 flex w-full"> <div className="text-md mt-8 mb-2 text-left">General Comments</div> <div className="mb-4 w-full border-b border-gray-200" /> - <ContractCommentsActivity - contract={contract} - comments={updatedComments.filter( - (comment) => - comment.answerOutcome === undefined && - comment.betId === undefined - )} - tips={tips} - /> + <ContractCommentInput className="mb-5" contract={contract} /> + {generalComments.map((comment) => ( + <FeedCommentThread + key={comment.id} + contract={contract} + parentComment={comment} + threadComments={[]} + tips={tips} + /> + ))} </Col> </> ) } else { + const commentsByParent = groupBy(comments, (c) => c.replyToCommentId ?? '_') + const topLevelComments = commentsByParent['_'] ?? [] return ( - <ContractCommentsActivity - contract={contract} - comments={comments} - tips={tips} - /> + <> + <ContractCommentInput className="mb-5" contract={contract} /> + {sortBy(topLevelComments, (c) => -c.createdTime).map((parent) => ( + <FeedCommentThread + key={parent.id} + contract={contract} + parentComment={parent} + threadComments={sortBy( + commentsByParent[parent.id] ?? [], + (c) => c.createdTime + )} + tips={tips} + /> + ))} + </> ) } }) -function ContractBetsActivity(props: { contract: Contract; bets: Bet[] }) { +const BetsTabContent = memo(function BetsTabContent(props: { + contract: Contract + bets: Bet[] +}) { const { contract, bets } = props const [page, setPage] = useState(0) const ITEMS_PER_PAGE = 50 @@ -133,6 +163,9 @@ function ContractBetsActivity(props: { contract: Contract; bets: Bet[] }) { const end = start + ITEMS_PER_PAGE const lps = useLiquidity(contract.id) ?? [] + const visibleBets = bets.filter( + (bet) => !bet.isAnte && !bet.isRedemption && bet.amount !== 0 + ) const visibleLps = lps.filter( (l) => !l.isAnte && @@ -142,7 +175,7 @@ function ContractBetsActivity(props: { contract: Contract; bets: Bet[] }) { ) const items = [ - ...bets.map((bet) => ({ + ...visibleBets.map((bet) => ({ type: 'bet' as const, id: bet.id + '-' + bet.isSold, bet, @@ -184,74 +217,4 @@ function ContractBetsActivity(props: { contract: Contract; bets: Bet[] }) { /> </> ) -} - -function ContractCommentsActivity(props: { - contract: Contract - comments: ContractComment[] - tips: CommentTipMap -}) { - const { contract, comments, tips } = props - const commentsByParentId = groupBy(comments, (c) => c.replyToCommentId ?? '_') - const topLevelComments = sortBy( - commentsByParentId['_'] ?? [], - (c) => -c.createdTime - ) - - return ( - <> - <ContractCommentInput className="mb-5" contract={contract} /> - {topLevelComments.map((parent) => ( - <FeedCommentThread - key={parent.id} - contract={contract} - parentComment={parent} - threadComments={sortBy( - commentsByParentId[parent.id] ?? [], - (c) => c.createdTime - )} - tips={tips} - /> - ))} - </> - ) -} - -function FreeResponseContractCommentsActivity(props: { - contract: FreeResponseContract - comments: ContractComment[] - tips: CommentTipMap -}) { - const { contract, comments, tips } = props - - const sortedAnswers = sortBy( - contract.answers, - (answer) => -getOutcomeProbability(contract, answer.number.toString()) - ) - const commentsByOutcome = groupBy( - comments, - (c) => c.answerOutcome ?? c.betOutcome ?? '_' - ) - - return ( - <> - {sortedAnswers.map((answer) => ( - <div key={answer.id} className="relative pb-4"> - <span - className="absolute top-5 left-5 -ml-px h-[calc(100%-2rem)] w-0.5 bg-gray-200" - aria-hidden="true" - /> - <FeedAnswerCommentGroup - contract={contract} - answer={answer} - answerComments={sortBy( - commentsByOutcome[answer.number.toString()] ?? [], - (c) => c.createdTime - )} - tips={tips} - /> - </div> - ))} - </> - ) -} +}) diff --git a/web/components/contract/extra-contract-actions-row.tsx b/web/components/contract/extra-contract-actions-row.tsx index af5db9c3..8f4b5579 100644 --- a/web/components/contract/extra-contract-actions-row.tsx +++ b/web/components/contract/extra-contract-actions-row.tsx @@ -1,6 +1,4 @@ -import clsx from 'clsx' import { ShareIcon } from '@heroicons/react/outline' - import { Row } from '../layout/row' import { Contract } from 'web/lib/firebase/contracts' import React, { useState } from 'react' @@ -10,7 +8,7 @@ import { ShareModal } from './share-modal' 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 { Tooltip } from '../tooltip' export function ExtraContractActionsRow(props: { contract: Contract }) { const { contract } = props @@ -23,27 +21,23 @@ export function ExtraContractActionsRow(props: { contract: Contract }) { {user?.id !== contract.creatorId && ( <LikeMarketButton contract={contract} user={user} /> )} - <Button - size="sm" - color="gray-white" - className={'flex'} - onClick={() => { - setShareOpen(true) - }} - > - <Row> - <ShareIcon className={clsx('h-5 w-5')} aria-hidden="true" /> - </Row> - <ShareModal - isOpen={isShareOpen} - setOpen={setShareOpen} - contract={contract} - user={user} - /> - </Button> - <Col className={'justify-center'}> - <ContractInfoDialog contract={contract} /> - </Col> + <Tooltip text="Share" placement="bottom" noTap noFade> + <Button + size="sm" + color="gray-white" + className={'flex'} + onClick={() => setShareOpen(true)} + > + <ShareIcon className="h-5 w-5" aria-hidden /> + <ShareModal + isOpen={isShareOpen} + setOpen={setShareOpen} + contract={contract} + user={user} + /> + </Button> + </Tooltip> + <ContractInfoDialog contract={contract} /> </Row> ) } diff --git a/web/components/contract/like-market-button.tsx b/web/components/contract/like-market-button.tsx index 01dce32f..7e0c765a 100644 --- a/web/components/contract/like-market-button.tsx +++ b/web/components/contract/like-market-button.tsx @@ -13,6 +13,7 @@ import { Col } from 'web/components/layout/col' import { firebaseLogin } from 'web/lib/firebase/users' import { useMarketTipTxns } from 'web/hooks/use-tip-txns' import { sum } from 'lodash' +import { Tooltip } from '../tooltip' export function LikeMarketButton(props: { contract: Contract @@ -37,37 +38,44 @@ export function LikeMarketButton(props: { } return ( - <Button - size={'sm'} - className={'max-w-xs self-center'} - color={'gray-white'} - onClick={onLike} + <Tooltip + text={`Tip ${formatMoney(LIKE_TIP_AMOUNT)}`} + placement="bottom" + noTap + noFade > - <Col className={'relative items-center sm:flex-row'}> - <HeartIcon - className={clsx( - '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))) - ? 'fill-red-500 text-red-500' - : '' - )} - /> - {totalTipped > 0 && ( - <div + <Button + size={'sm'} + className={'max-w-xs self-center'} + color={'gray-white'} + onClick={onLike} + > + <Col className={'relative items-center sm:flex-row'}> + <HeartIcon 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]' + '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))) + ? 'fill-red-500 text-red-500' + : '' )} - > - {totalTipped} - </div> - )} - </Col> - </Button> + /> + {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> + </Tooltip> ) } diff --git a/web/components/contract/share-modal.tsx b/web/components/contract/share-modal.tsx index e1eb26eb..72c7aba3 100644 --- a/web/components/contract/share-modal.tsx +++ b/web/components/contract/share-modal.tsx @@ -21,6 +21,7 @@ import { CreateChallengeModal } from 'web/components/challenges/create-challenge import { useState } from 'react' import { CHALLENGES_ENABLED } from 'common/challenge' import ChallengeIcon from 'web/lib/icons/challenge-icon' +import { QRCode } from '../qr-code' export function ShareModal(props: { contract: Contract @@ -54,6 +55,12 @@ export function ShareModal(props: { </SiteLink>{' '} if a new user signs up using the link! </p> + <QRCode + url={shareUrl} + className="self-center" + width={150} + height={150} + /> <Button size="2xl" color="indigo" diff --git a/web/components/feed/feed-answer-comment-group.tsx b/web/components/feed/feed-answer-comment-group.tsx index 27f0f731..e17ea578 100644 --- a/web/components/feed/feed-answer-comment-group.tsx +++ b/web/components/feed/feed-answer-comment-group.tsx @@ -1,7 +1,7 @@ import { Answer } from 'common/answer' import { FreeResponseContract } from 'common/contract' import { ContractComment } from 'common/comment' -import React, { useEffect, useState } from 'react' +import React, { useEffect, useRef, useState } from 'react' import { Col } from 'web/components/layout/col' import { Row } from 'web/components/layout/row' import { Avatar } from 'web/components/avatar' @@ -10,11 +10,10 @@ import clsx from 'clsx' import { ContractCommentInput, FeedComment, + ReplyTo, } from 'web/components/feed/feed-comments' import { CopyLinkDateTimeComponent } from 'web/components/feed/copy-link-date-time' import { useRouter } from 'next/router' -import { User } from 'common/user' -import { useEvent } from 'web/hooks/use-event' import { CommentTipMap } from 'web/hooks/use-tip-txns' import { UserLink } from 'web/components/user-link' @@ -27,32 +26,17 @@ export function FeedAnswerCommentGroup(props: { const { answer, contract, answerComments, tips } = props const { username, avatarUrl, name, text } = answer - const [replyToUser, setReplyToUser] = - useState<Pick<User, 'id' | 'username'>>() - const [showReply, setShowReply] = useState(false) - const [highlighted, setHighlighted] = useState(false) + const [replyTo, setReplyTo] = useState<ReplyTo>() const router = useRouter() - const answerElementId = `answer-${answer.id}` - - const scrollAndOpenReplyInput = useEvent( - (comment?: ContractComment, answer?: Answer) => { - setReplyToUser( - comment - ? { id: comment.userId, username: comment.userUsername } - : answer - ? { id: answer.userId, username: answer.username } - : undefined - ) - setShowReply(true) - } - ) + const highlighted = router.asPath.endsWith(`#${answerElementId}`) + const answerRef = useRef<HTMLDivElement>(null) useEffect(() => { - if (router.asPath.endsWith(`#${answerElementId}`)) { - setHighlighted(true) + if (highlighted && answerRef.current != null) { + answerRef.current.scrollIntoView(true) } - }, [answerElementId, router.asPath]) + }, [highlighted]) return ( <Col className="relative flex-1 items-stretch gap-3"> @@ -61,6 +45,7 @@ export function FeedAnswerCommentGroup(props: { 'gap-3 space-x-3 pt-4 transition-all duration-1000', highlighted ? `-m-2 my-3 rounded bg-indigo-500/[0.2] p-2` : '' )} + ref={answerRef} id={answerElementId} > <Avatar username={username} avatarUrl={avatarUrl} /> @@ -83,7 +68,9 @@ export function FeedAnswerCommentGroup(props: { <div className="sm:hidden"> <button className="text-xs font-bold text-gray-500 hover:underline" - onClick={() => scrollAndOpenReplyInput(undefined, answer)} + onClick={() => + setReplyTo({ id: answer.id, username: answer.username }) + } > Reply </button> @@ -92,7 +79,9 @@ export function FeedAnswerCommentGroup(props: { <div className="justify-initial hidden sm:block"> <button className="text-xs font-bold text-gray-500 hover:underline" - onClick={() => scrollAndOpenReplyInput(undefined, answer)} + onClick={() => + setReplyTo({ id: answer.id, username: answer.username }) + } > Reply </button> @@ -107,11 +96,13 @@ export function FeedAnswerCommentGroup(props: { contract={contract} comment={comment} tips={tips[comment.id] ?? {}} - onReplyClick={scrollAndOpenReplyInput} + onReplyClick={() => + setReplyTo({ id: comment.id, username: comment.userUsername }) + } /> ))} </Col> - {showReply && ( + {replyTo && ( <div className="relative ml-7"> <span className="absolute -left-1 -ml-[1px] mt-[1.25rem] h-2 w-0.5 rotate-90 bg-gray-200" @@ -120,8 +111,8 @@ export function FeedAnswerCommentGroup(props: { <ContractCommentInput contract={contract} parentAnswerOutcome={answer.number.toString()} - replyToUser={replyToUser} - onSubmitComment={() => setShowReply(false)} + replyTo={replyTo} + onSubmitComment={() => setReplyTo(undefined)} /> </div> )} diff --git a/web/components/feed/feed-comments.tsx b/web/components/feed/feed-comments.tsx index acb48ec1..1b62690b 100644 --- a/web/components/feed/feed-comments.tsx +++ b/web/components/feed/feed-comments.tsx @@ -1,6 +1,6 @@ import { ContractComment } from 'common/comment' import { Contract } from 'common/contract' -import React, { useEffect, useState } from 'react' +import React, { useEffect, useRef, useState } from 'react' import { useUser } from 'web/hooks/use-user' import { formatMoney } from 'common/util/format' import { useRouter } from 'next/router' @@ -20,6 +20,8 @@ import { Editor } from '@tiptap/react' import { UserLink } from 'web/components/user-link' import { CommentInput } from '../comment-input' +export type ReplyTo = { id: string; username: string } + export function FeedCommentThread(props: { contract: Contract threadComments: ContractComment[] @@ -27,13 +29,7 @@ export function FeedCommentThread(props: { parentComment: ContractComment }) { const { contract, threadComments, tips, parentComment } = props - const [showReply, setShowReply] = useState(false) - const [replyTo, setReplyTo] = useState<{ id: string; username: string }>() - - function scrollAndOpenReplyInput(comment: ContractComment) { - setReplyTo({ id: comment.userId, username: comment.userUsername }) - setShowReply(true) - } + const [replyTo, setReplyTo] = useState<ReplyTo>() return ( <Col className="relative w-full items-stretch gap-3 pb-4"> @@ -48,10 +44,12 @@ export function FeedCommentThread(props: { contract={contract} comment={comment} tips={tips[comment.id] ?? {}} - onReplyClick={scrollAndOpenReplyInput} + onReplyClick={() => + setReplyTo({ id: comment.id, username: comment.userUsername }) + } /> ))} - {showReply && ( + {replyTo && ( <Col className="-pb-2 relative ml-6"> <span className="absolute -left-1 -ml-[1px] mt-[0.8rem] h-2 w-0.5 rotate-90 bg-gray-200" @@ -60,10 +58,8 @@ export function FeedCommentThread(props: { <ContractCommentInput contract={contract} parentCommentId={parentComment.id} - replyToUser={replyTo} - onSubmitComment={() => { - setShowReply(false) - }} + replyTo={replyTo} + onSubmitComment={() => setReplyTo(undefined)} /> </Col> )} @@ -76,7 +72,7 @@ export function FeedComment(props: { comment: ContractComment tips?: CommentTips indent?: boolean - onReplyClick?: (comment: ContractComment) => void + onReplyClick?: () => void }) { const { contract, comment, tips, indent, onReplyClick } = props const { @@ -98,16 +94,19 @@ export function FeedComment(props: { money = formatMoney(Math.abs(comment.betAmount)) } - const [highlighted, setHighlighted] = useState(false) const router = useRouter() + const highlighted = router.asPath.endsWith(`#${comment.id}`) + const commentRef = useRef<HTMLDivElement>(null) + useEffect(() => { - if (router.asPath.endsWith(`#${comment.id}`)) { - setHighlighted(true) + if (highlighted && commentRef.current != null) { + commentRef.current.scrollIntoView(true) } - }, [comment.id, router.asPath]) + }, [highlighted]) return ( <Row + ref={commentRef} id={comment.id} className={clsx( 'relative', @@ -174,7 +173,7 @@ export function FeedComment(props: { {onReplyClick && ( <button className="font-bold hover:underline" - onClick={() => onReplyClick(comment)} + onClick={onReplyClick} > Reply </button> @@ -204,7 +203,7 @@ export function ContractCommentInput(props: { contract: Contract className?: string parentAnswerOutcome?: string | undefined - replyToUser?: { id: string; username: string } + replyTo?: ReplyTo parentCommentId?: string onSubmitComment?: () => void }) { @@ -226,7 +225,7 @@ export function ContractCommentInput(props: { return ( <CommentInput - replyToUser={props.replyToUser} + replyTo={props.replyTo} parentAnswerOutcome={props.parentAnswerOutcome} parentCommentId={props.parentCommentId} onSubmitComment={onSubmitComment} diff --git a/web/components/follow-market-button.tsx b/web/components/follow-market-button.tsx index 0e65165b..319d4af6 100644 --- a/web/components/follow-market-button.tsx +++ b/web/components/follow-market-button.tsx @@ -14,6 +14,7 @@ import { track } from 'web/lib/service/analytics' import { WatchMarketModal } from 'web/components/contract/watch-market-modal' import { useState } from 'react' import { Col } from 'web/components/layout/col' +import { Tooltip } from './tooltip' export const FollowMarketButton = (props: { contract: Contract @@ -23,61 +24,70 @@ export const FollowMarketButton = (props: { const followers = useContractFollows(contract.id) const [open, setOpen] = useState(false) + const watching = followers?.includes(user?.id ?? 'nope') + return ( - <Button - size={'sm'} - color={'gray-white'} - onClick={async () => { - if (!user) return firebaseLogin() - if (followers?.includes(user.id)) { - await unFollowContract(contract.id, user.id) - toast("You'll no longer receive notifications from this market", { - icon: <CheckIcon className={'text-primary h-5 w-5'} />, - }) - track('Unwatch Market', { - slug: contract.slug, - }) - } else { - await followContract(contract.id, user.id) - toast("You'll now receive notifications from this market!", { - icon: <CheckIcon className={'text-primary h-5 w-5'} />, - }) - track('Watch Market', { - slug: contract.slug, - }) - } - if (!user.hasSeenContractFollowModal) { - await updateUser(user.id, { - hasSeenContractFollowModal: true, - }) - setOpen(true) - } - }} + <Tooltip + text={watching ? 'Unfollow' : 'Follow'} + placement="bottom" + noTap + noFade > - {followers?.includes(user?.id ?? 'nope') ? ( - <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" - /> - {/* 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" - /> - {/* Watch */} - </Col> - )} - <WatchMarketModal - open={open} - setOpen={setOpen} - title={`You ${ - followers?.includes(user?.id ?? 'nope') ? 'watched' : 'unwatched' - } a question!`} - /> - </Button> + <Button + size={'sm'} + color={'gray-white'} + onClick={async () => { + if (!user) return firebaseLogin() + if (followers?.includes(user.id)) { + await unFollowContract(contract.id, user.id) + toast("You'll no longer receive notifications from this market", { + icon: <CheckIcon className={'text-primary h-5 w-5'} />, + }) + track('Unwatch Market', { + slug: contract.slug, + }) + } else { + await followContract(contract.id, user.id) + toast("You'll now receive notifications from this market!", { + icon: <CheckIcon className={'text-primary h-5 w-5'} />, + }) + track('Watch Market', { + slug: contract.slug, + }) + } + if (!user.hasSeenContractFollowModal) { + await updateUser(user.id, { + hasSeenContractFollowModal: true, + }) + setOpen(true) + } + }} + > + {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" + /> + {/* 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" + /> + {/* Watch */} + </Col> + )} + <WatchMarketModal + open={open} + setOpen={setOpen} + title={`You ${ + followers?.includes(user?.id ?? 'nope') ? 'watched' : 'unwatched' + } a question!`} + /> + </Button> + </Tooltip> ) } diff --git a/web/components/following-button.tsx b/web/components/following-button.tsx index fdf739a1..135f43a8 100644 --- a/web/components/following-button.tsx +++ b/web/components/following-button.tsx @@ -115,6 +115,7 @@ function FollowsDialog(props: { <div className="p-2 pb-1 text-xl">{user.name}</div> <div className="p-2 pt-0 text-sm text-gray-500">@{user.username}</div> <Tabs + className="mb-4" tabs={[ { title: 'Following', diff --git a/web/components/landing-page-panel.tsx b/web/components/landing-page-panel.tsx index f0dae17d..54e501b2 100644 --- a/web/components/landing-page-panel.tsx +++ b/web/components/landing-page-panel.tsx @@ -23,6 +23,7 @@ export function LandingPagePanel(props: { hotContracts: Contract[] }) { height={250} width={250} className="self-center" + alt="Manifold logo" src="/flappy-logo.gif" /> <div className="m-4 max-w-[550px] self-center"> diff --git a/web/components/layout/tabs.tsx b/web/components/layout/tabs.tsx index 3d72b13c..980a3cfc 100644 --- a/web/components/layout/tabs.tsx +++ b/web/components/layout/tabs.tsx @@ -31,7 +31,7 @@ export function ControlledTabs(props: TabProps & { activeIndex: number }) { return ( <> <nav - className={clsx('mb-4 space-x-8 border-b border-gray-200', className)} + className={clsx('space-x-8 border-b border-gray-200', className)} aria-label="Tabs" > {tabs.map((tab, i) => ( diff --git a/web/components/leaderboard.tsx b/web/components/leaderboard.tsx index a0670795..1035e9d1 100644 --- a/web/components/leaderboard.tsx +++ b/web/components/leaderboard.tsx @@ -1,28 +1,33 @@ import clsx from 'clsx' -import { User } from 'common/user' import { Avatar } from './avatar' import { Row } from './layout/row' import { SiteLink } from './site-link' import { Title } from './title' -export function Leaderboard(props: { +interface LeaderboardEntry { + username: string + name: string + avatarUrl?: string +} + +export function Leaderboard<T extends LeaderboardEntry>(props: { title: string - users: User[] + entries: T[] columns: { header: string - renderCell: (user: User) => any + renderCell: (entry: T) => any }[] className?: string maxToShow?: number }) { // TODO: Ideally, highlight your own entry on the leaderboard const { title, columns, className } = props - const maxToShow = props.maxToShow ?? props.users.length - const users = props.users.slice(0, maxToShow) + const maxToShow = props.maxToShow ?? props.entries.length + const entries = props.entries.slice(0, maxToShow) return ( <div className={clsx('w-full px-1', className)}> <Title text={title} className="!mt-0" /> - {users.length === 0 ? ( + {entries.length === 0 ? ( <div className="ml-2 text-gray-500">None yet</div> ) : ( <div className="overflow-x-auto"> @@ -37,19 +42,19 @@ export function Leaderboard(props: { </tr> </thead> <tbody> - {users.map((user, index) => ( - <tr key={user.id}> + {entries.map((entry, index) => ( + <tr key={index}> <td>{index + 1}</td> <td className="max-w-[190px]"> - <SiteLink className="relative" href={`/${user.username}`}> + <SiteLink className="relative" href={`/${entry.username}`}> <Row className="items-center gap-4"> - <Avatar avatarUrl={user.avatarUrl} size={8} /> - <div className="truncate">{user.name}</div> + <Avatar avatarUrl={entry.avatarUrl} size={8} /> + <div className="truncate">{entry.name}</div> </Row> </SiteLink> </td> {columns.map((column) => ( - <td key={column.header}>{column.renderCell(user)}</td> + <td key={column.header}>{column.renderCell(entry)}</td> ))} </tr> ))} diff --git a/web/components/nav/group-nav-bar.tsx b/web/components/nav/group-nav-bar.tsx deleted file mode 100644 index 9ea3f5a4..00000000 --- a/web/components/nav/group-nav-bar.tsx +++ /dev/null @@ -1,94 +0,0 @@ -import { ClipboardIcon, HomeIcon } from '@heroicons/react/outline' -import { Item } from './sidebar-item' - -import clsx from 'clsx' -import { trackCallback } from 'web/lib/service/analytics' -import TrophyIcon from 'web/lib/icons/trophy-icon' -import { useUser } from 'web/hooks/use-user' -import NotificationsIcon from '../notifications-icon' -import router from 'next/router' -import { userProfileItem } from './bottom-nav-bar' - -const mobileGroupNavigation = [ - { name: 'Markets', key: 'markets', icon: HomeIcon }, - { name: 'Leaderboard', key: 'leaderboards', icon: TrophyIcon }, - { name: 'About', key: 'about', icon: ClipboardIcon }, -] - -const mobileGeneralNavigation = [ - { - name: 'Notifications', - key: 'notifications', - icon: NotificationsIcon, - href: '/notifications', - }, -] - -export function GroupNavBar(props: { - currentPage: string - onClick: (key: string) => void -}) { - const { currentPage } = props - const user = useUser() - - return ( - <nav className="z-20 flex justify-between border-t-2 bg-white text-xs text-gray-700 lg:hidden"> - {mobileGroupNavigation.map((item) => ( - <NavBarItem - key={item.name} - item={item} - currentPage={currentPage} - onClick={props.onClick} - /> - ))} - - {mobileGeneralNavigation.map((item) => ( - <NavBarItem - key={item.name} - item={item} - currentPage={currentPage} - onClick={() => { - router.push(item.href) - }} - /> - ))} - - {user && ( - <NavBarItem - key={'profile'} - currentPage={currentPage} - onClick={() => { - router.push(`/${user.username}?tab=trades`) - }} - item={userProfileItem(user)} - /> - )} - </nav> - ) -} - -function NavBarItem(props: { - item: Item - currentPage: string - onClick: (key: string) => void -}) { - const { item, currentPage } = props - const track = trackCallback( - `group navbar: ${item.trackingEventName ?? item.name}` - ) - - return ( - <button onClick={() => props.onClick(item.key ?? '#')}> - <a - className={clsx( - 'block w-full py-1 px-3 text-center hover:bg-indigo-200 hover:text-indigo-700', - currentPage === item.key && 'bg-gray-200 text-indigo-700' - )} - onClick={track} - > - {item.icon && <item.icon className="my-1 mx-auto h-6 w-6" />} - {item.name} - </a> - </button> - ) -} diff --git a/web/components/nav/group-sidebar.tsx b/web/components/nav/group-sidebar.tsx deleted file mode 100644 index a68064e0..00000000 --- a/web/components/nav/group-sidebar.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import { ClipboardIcon, HomeIcon } from '@heroicons/react/outline' -import clsx from 'clsx' -import { useUser } from 'web/hooks/use-user' -import { ManifoldLogo } from './manifold-logo' -import { ProfileSummary } from './profile-menu' -import React from 'react' -import TrophyIcon from 'web/lib/icons/trophy-icon' -import { SignInButton } from '../sign-in-button' -import NotificationsIcon from '../notifications-icon' -import { SidebarItem } from './sidebar-item' -import { buildArray } from 'common/util/array' -import { User } from 'common/user' -import { Row } from '../layout/row' -import { Spacer } from '../layout/spacer' - -const groupNavigation = [ - { name: 'Markets', key: 'markets', icon: HomeIcon }, - { name: 'About', key: 'about', icon: ClipboardIcon }, - { name: 'Leaderboard', key: 'leaderboards', icon: TrophyIcon }, -] - -const generalNavigation = (user?: User | null) => - buildArray( - user && { - name: 'Notifications', - href: `/notifications`, - key: 'notifications', - icon: NotificationsIcon, - } - ) - -export function GroupSidebar(props: { - groupName: string - className?: string - onClick: (key: string) => void - joinOrAddQuestionsButton: React.ReactNode - currentKey: string -}) { - const { className, groupName, currentKey } = props - - const user = useUser() - - return ( - <nav - aria-label="Group Sidebar" - className={clsx('flex max-h-[100vh] flex-col', className)} - > - <ManifoldLogo className="pt-6" twoLine /> - <Row className="pl-2 text-xl text-indigo-700 sm:mt-3">{groupName}</Row> - - <div className=" min-h-0 shrink flex-col items-stretch gap-1 pt-6 lg:flex "> - {user ? ( - <ProfileSummary user={user} /> - ) : ( - <SignInButton className="mb-4" /> - )} - </div> - - {/* Desktop navigation */} - {groupNavigation.map((item) => ( - <SidebarItem - key={item.key} - item={item} - currentPage={currentKey} - onClick={props.onClick} - /> - ))} - {generalNavigation(user).map((item) => ( - <SidebarItem - key={item.key} - item={item} - currentPage={currentKey} - onClick={props.onClick} - /> - ))} - - <Spacer h={2} /> - - {props.joinOrAddQuestionsButton} - </nav> - ) -} diff --git a/web/components/nav/sidebar.tsx b/web/components/nav/sidebar.tsx index 45347774..b0a9862b 100644 --- a/web/components/nav/sidebar.tsx +++ b/web/components/nav/sidebar.tsx @@ -26,9 +26,14 @@ import TrophyIcon from 'web/lib/icons/trophy-icon' import { SignInButton } from '../sign-in-button' import { SidebarItem } from './sidebar-item' import { MoreButton } from './more-button' +import { Row } from '../layout/row' +import { Spacer } from '../layout/spacer' -export default function Sidebar(props: { className?: string }) { - const { className } = props +export default function Sidebar(props: { + className?: string + logoSubheading?: string +}) { + const { className, logoSubheading } = props const router = useRouter() const currentPage = router.pathname @@ -51,7 +56,13 @@ export default function Sidebar(props: { className?: string }) { aria-label="Sidebar" className={clsx('flex max-h-[100vh] flex-col', className)} > - <ManifoldLogo className="py-6" twoLine /> + <ManifoldLogo className="pt-6" twoLine /> + {logoSubheading && ( + <Row className="pl-2 text-2xl text-indigo-700 sm:mt-3"> + {logoSubheading} + </Row> + )} + <Spacer h={6} /> {!user && <SignInButton className="mb-4" />} diff --git a/web/components/onboarding/welcome.tsx b/web/components/onboarding/welcome.tsx index b18ef83f..eb51f2de 100644 --- a/web/components/onboarding/welcome.tsx +++ b/web/components/onboarding/welcome.tsx @@ -99,8 +99,6 @@ const useIsTwitch = (user: User | null | undefined) => { const isTwitch = router.pathname === '/twitch' useEffect(() => { - console.log('twich?', isTwitch) - if (isTwitch && user?.shouldShowWelcome) { updateUser(user.id, { ['shouldShowWelcome']: false }) } diff --git a/web/components/page.tsx b/web/components/page.tsx index 9b26e9f8..f72db80e 100644 --- a/web/components/page.tsx +++ b/web/components/page.tsx @@ -9,8 +9,15 @@ export function Page(props: { className?: string rightSidebarClassName?: string children?: ReactNode + logoSubheading?: string }) { - const { children, rightSidebar, className, rightSidebarClassName } = props + const { + children, + rightSidebar, + className, + rightSidebarClassName, + logoSubheading, + } = props const bottomBarPadding = 'pb-[58px] lg:pb-0 ' return ( @@ -23,7 +30,10 @@ export function Page(props: { )} > <Toaster /> - <Sidebar className="sticky top-0 hidden divide-gray-300 self-start pl-2 lg:col-span-2 lg:flex" /> + <Sidebar + logoSubheading={logoSubheading} + className="sticky top-0 hidden divide-gray-300 self-start pl-2 lg:col-span-2 lg:flex" + /> <main className={clsx( 'lg:col-span-8 lg:pt-6', diff --git a/web/components/referrals-button.tsx b/web/components/referrals-button.tsx index 4b4f7095..b164e10c 100644 --- a/web/components/referrals-button.tsx +++ b/web/components/referrals-button.tsx @@ -64,6 +64,7 @@ function ReferralsDialog(props: { <div className="p-2 pb-1 text-xl">{user.name}</div> <div className="p-2 pt-0 text-sm text-gray-500">@{user.username}</div> <Tabs + className="mb-4" tabs={[ { title: 'Referrals', diff --git a/web/components/tipper.tsx b/web/components/tipper.tsx index 7aef6189..46a988f6 100644 --- a/web/components/tipper.tsx +++ b/web/components/tipper.tsx @@ -16,6 +16,8 @@ import { track } from 'web/lib/service/analytics' import { Row } from './layout/row' import { Tooltip } from './tooltip' +const TIP_SIZE = 10 + export function Tipper(prop: { comment: Comment; tips: CommentTips }) { const { comment, tips } = prop @@ -82,9 +84,12 @@ export function Tipper(prop: { comment: Comment; tips: CommentTips }) { const canUp = me && me.id !== comment.userId && me.balance >= localTip + 5 return ( <Row className="items-center gap-0.5"> - <DownTip onClick={canDown ? () => addTip(-5) : undefined} /> + <DownTip onClick={canDown ? () => addTip(-TIP_SIZE) : undefined} /> <span className="font-bold">{Math.floor(total)}</span> - <UpTip onClick={canUp ? () => addTip(+5) : undefined} value={localTip} /> + <UpTip + onClick={canUp ? () => addTip(+TIP_SIZE) : undefined} + value={localTip} + /> {localTip === 0 ? ( '' ) : ( @@ -107,7 +112,7 @@ function DownTip(props: { onClick?: () => void }) { <Tooltip className="h-6 w-6" placement="bottom" - text={onClick && `-${formatMoney(5)}`} + text={onClick && `-${formatMoney(TIP_SIZE)}`} noTap > <button @@ -128,7 +133,7 @@ function UpTip(props: { onClick?: () => void; value: number }) { <Tooltip className="h-6 w-6" placement="bottom" - text={onClick && `Tip ${formatMoney(5)}`} + text={onClick && `Tip ${formatMoney(TIP_SIZE)}`} noTap > <button diff --git a/web/components/user-page.tsx b/web/components/user-page.tsx index 2b24fa60..f9845fbe 100644 --- a/web/components/user-page.tsx +++ b/web/components/user-page.tsx @@ -254,6 +254,7 @@ export function UserPage(props: { user: User }) { </Row> )} <QueryUncontrolledTabs + className="mb-4" currentPageForAnalytics={'profile'} labelClassName={'pb-2 pt-1 '} tabs={[ @@ -283,7 +284,7 @@ export function UserPage(props: { user: User }) { title: 'Stats', content: ( <Col className="mb-8"> - <Row className={'mb-8 flex-wrap items-center gap-6'}> + <Row className="mb-8 flex-wrap items-center gap-x-6 gap-y-2"> <FollowingButton user={user} /> <FollowersButton user={user} /> <ReferralsButton user={user} /> diff --git a/web/next.config.js b/web/next.config.js index 21b375ba..cf727fd4 100644 --- a/web/next.config.js +++ b/web/next.config.js @@ -9,9 +9,6 @@ module.exports = { reactStrictMode: true, optimizeFonts: false, experimental: { - images: { - allowFutureImage: true, - }, scrollRestoration: true, externalDir: true, modularizeImports: { diff --git a/web/package.json b/web/package.json index ba25a6e1..6ee29183 100644 --- a/web/package.json +++ b/web/package.json @@ -46,7 +46,7 @@ "gridjs-react": "5.0.2", "lodash": "4.17.21", "nanoid": "^3.3.4", - "next": "12.2.5", + "next": "12.3.1", "node-fetch": "3.2.4", "prosemirror-state": "1.4.1", "react": "17.0.2", diff --git a/web/pages/[username]/[contractSlug].tsx b/web/pages/[username]/[contractSlug].tsx index 3682e700..38df2fbf 100644 --- a/web/pages/[username]/[contractSlug].tsx +++ b/web/pages/[username]/[contractSlug].tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useMemo, useState } from 'react' +import React, { memo, useEffect, useMemo, useState } from 'react' import { ArrowLeftIcon } from '@heroicons/react/outline' import { useContractWithPreload } from 'web/hooks/use-contract' @@ -17,7 +17,6 @@ import { import { SEO } from 'web/components/SEO' import { Page } from 'web/components/page' import { Bet, listAllBets } from 'web/lib/firebase/bets' -import { listAllComments } from 'web/lib/firebase/comments' import Custom404 from '../404' import { AnswersPanel } from 'web/components/answers/answers-panel' import { fromPropz, usePropz } from 'web/hooks/use-propz' @@ -32,8 +31,6 @@ import { CPMMBinaryContract } from 'common/contract' import { AlertBox } from 'web/components/alert-box' import { useTracking } from 'web/hooks/use-tracking' import { useSaveReferral } from 'web/hooks/use-save-referral' -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 { @@ -54,25 +51,14 @@ export const getStaticProps = fromPropz(getStaticPropz) export async function getStaticPropz(props: { params: { username: string; contractSlug: string } }) { - const { username, contractSlug } = props.params + const { contractSlug } = props.params const contract = (await getContractFromSlug(contractSlug)) || null const contractId = contract?.id - - const [bets, comments] = await Promise.all([ - contractId ? listAllBets(contractId) : [], - contractId ? listAllComments(contractId) : [], - ]) + const bets = contractId ? await listAllBets(contractId) : [] return { - props: { - contract, - username, - slug: contractSlug, - // Limit the data sent to the client. Client will still load all bets and comments directly. - bets: bets.slice(0, 5000), - comments: comments.slice(0, 1000), - }, - + // Limit the data sent to the client. Client will still load all bets directly. + props: { contract, bets: bets.slice(0, 5000) }, revalidate: 5, // regenerate after five seconds } } @@ -83,21 +69,11 @@ export async function getStaticPaths() { export default function ContractPage(props: { contract: Contract | null - username: string bets: Bet[] - comments: ContractComment[] - slug: string backToHome?: () => void }) { - props = usePropz(props, getStaticPropz) ?? { - contract: null, - username: '', - comments: [], - bets: [], - slug: '', - } + props = usePropz(props, getStaticPropz) ?? { contract: null, bets: [] } - const user = useUser() const inIframe = useIsIframe() if (inIframe) { return <ContractEmbedPage {...props} /> @@ -109,9 +85,7 @@ export default function ContractPage(props: { return <Custom404 /> } - return ( - <ContractPageContent key={contract.id} {...{ ...props, contract, user }} /> - ) + return <ContractPageContent key={contract.id} {...{ ...props, contract }} /> } // requires an admin to resolve a week after market closes @@ -119,12 +93,10 @@ export function needsAdminToResolve(contract: Contract) { return !contract.isResolved && dayjs().diff(contract.closeTime, 'day') > 7 } -export function ContractPageSidebar(props: { - user: User | null | undefined - contract: Contract -}) { - const { contract, user } = props +export function ContractPageSidebar(props: { contract: Contract }) { + const { contract } = props const { creatorId, isResolved, outcomeType } = contract + const user = useUser() const isCreator = user?.id === creatorId const isBinary = outcomeType === 'BINARY' const isPseudoNumeric = outcomeType === 'PSEUDO_NUMERIC' @@ -173,11 +145,11 @@ export function ContractPageSidebar(props: { export function ContractPageContent( props: Parameters<typeof ContractPage>[0] & { contract: Contract - user?: User | null } ) { - const { backToHome, comments, user } = props + const { backToHome } = props const contract = useContractWithPreload(props.contract) ?? props.contract + const user = useUser() usePrefetch(user?.id) useTracking( 'view market', @@ -217,9 +189,8 @@ export function ContractPageContent( contractId: contract.id, }) - const rightSidebar = <ContractPageSidebar user={user} contract={contract} /> return ( - <Page rightSidebar={rightSidebar}> + <Page rightSidebar={<ContractPageSidebar contract={contract} />}> {showConfetti && ( <FullscreenConfetti recycle={false} numberOfPieces={300} /> )} @@ -228,7 +199,7 @@ export function ContractPageContent( <SEO title={question} description={ogCardProps.description} - url={`/${props.username}/${props.slug}`} + url={`/${contract.creatorUsername}/${contract.slug}`} ogCardProps={ogCardProps} /> )} @@ -271,22 +242,13 @@ export function ContractPageContent( <> <div className="grid grid-cols-1 sm:grid-cols-2"> <ContractLeaderboard contract={contract} bets={bets} /> - <ContractTopTrades - contract={contract} - bets={bets} - comments={comments} - /> + <ContractTopTrades contract={contract} bets={bets} /> </div> <Spacer h={12} /> </> )} - <ContractTabs - contract={contract} - user={user} - bets={bets} - comments={comments} - /> + <ContractTabs contract={contract} bets={bets} /> {!user ? ( <Col className="mt-4 max-w-sm items-center xl:hidden"> <BetSignUpPrompt /> @@ -307,26 +269,28 @@ export function ContractPageContent( ) } -function RecommendedContractsWidget(props: { contract: Contract }) { - const { contract } = props - const user = useUser() - const [recommendations, setRecommendations] = useState<Contract[]>([]) - useEffect(() => { - if (user) { - getRecommendedContracts(contract, user.id, 6).then(setRecommendations) +const RecommendedContractsWidget = memo( + function RecommendedContractsWidget(props: { contract: Contract }) { + const { contract } = props + const user = useUser() + const [recommendations, setRecommendations] = useState<Contract[]>([]) + useEffect(() => { + if (user) { + getRecommendedContracts(contract, user.id, 6).then(setRecommendations) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [contract.id, user?.id]) + if (recommendations.length === 0) { + return null } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [contract.id, user?.id]) - if (recommendations.length === 0) { - return null + return ( + <Col className="mt-2 gap-2 px-2 sm:px-0"> + <Title className="text-gray-700" text="Recommended" /> + <ContractsGrid + contracts={recommendations} + trackingPostfix=" recommended" + /> + </Col> + ) } - return ( - <Col className="mt-2 gap-2 px-2 sm:px-0"> - <Title className="text-gray-700" text="Recommended" /> - <ContractsGrid - contracts={recommendations} - trackingPostfix=" recommended" - /> - </Col> - ) -} +) diff --git a/web/pages/_app.tsx b/web/pages/_app.tsx index d5a38272..3e82d029 100644 --- a/web/pages/_app.tsx +++ b/web/pages/_app.tsx @@ -4,7 +4,7 @@ import { useEffect } from 'react' import Head from 'next/head' import Script from 'next/script' import { QueryClient, QueryClientProvider } from 'react-query' -import { AuthProvider } from 'web/components/auth-context' +import { AuthProvider, AuthUser } from 'web/components/auth-context' import Welcome from 'web/components/onboarding/welcome' function firstLine(msg: string) { @@ -24,7 +24,10 @@ function printBuildInfo() { } } -function MyApp({ Component, pageProps }: AppProps) { +// specially treated props that may be present in the server/static props +type ManifoldPageProps = { auth?: AuthUser } + +function MyApp({ Component, pageProps }: AppProps<ManifoldPageProps>) { useEffect(printBuildInfo, []) return ( @@ -78,7 +81,7 @@ function MyApp({ Component, pageProps }: AppProps) { </Head> <AuthProvider serverUser={pageProps.auth}> <QueryClientProvider client={queryClient}> - <Welcome {...pageProps} /> + <Welcome /> <Component {...pageProps} /> </QueryClientProvider> </AuthProvider> diff --git a/web/pages/add-funds.tsx b/web/pages/add-funds.tsx index ed25a21a..602de276 100644 --- a/web/pages/add-funds.tsx +++ b/web/pages/add-funds.tsx @@ -24,14 +24,14 @@ export default function AddFundsPage() { return ( <Page> <SEO - title="Get Manifold Dollars" - description="Get Manifold Dollars" + title="Get Mana" + description="Buy mana to trade in your favorite markets on Manifold" url="/add-funds" /> <Col className="items-center"> <Col className="h-full rounded bg-white p-4 py-8 sm:p-8 sm:shadow-md"> - <Title className="!mt-0" text="Get Manifold Dollars" /> + <Title className="!mt-0" text="Get Mana" /> <img className="mb-6 block -scale-x-100 self-center" src="/stylized-crane-black.png" @@ -40,8 +40,8 @@ export default function AddFundsPage() { /> <div className="mb-6 text-gray-500"> - Purchase Manifold Dollars to trade in your favorite markets. <br />{' '} - (Not redeemable for cash.) + Buy mana (M$) to trade in your favorite markets. <br /> (Not + redeemable for cash.) </div> <div className="mb-2 text-sm text-gray-500">Amount</div> diff --git a/web/pages/challenges/index.tsx b/web/pages/challenges/index.tsx index 11d0f9ab..16999aaa 100644 --- a/web/pages/challenges/index.tsx +++ b/web/pages/challenges/index.tsx @@ -92,7 +92,7 @@ export default function ChallengesListPage() { tap the button above to create a new market & challenge in one. </p> - <Tabs tabs={[...userTab, ...publicTab]} /> + <Tabs className="mb-4" tabs={[...userTab, ...publicTab]} /> </Col> </Page> ) diff --git a/web/pages/embed/[username]/[contractSlug].tsx b/web/pages/embed/[username]/[contractSlug].tsx index 62dd1ae1..75a9ad05 100644 --- a/web/pages/embed/[username]/[contractSlug].tsx +++ b/web/pages/embed/[username]/[contractSlug].tsx @@ -34,20 +34,14 @@ export const getStaticProps = fromPropz(getStaticPropz) export async function getStaticPropz(props: { params: { username: string; contractSlug: string } }) { - const { username, contractSlug } = props.params + const { contractSlug } = props.params const contract = (await getContractFromSlug(contractSlug)) || null const contractId = contract?.id const bets = contractId ? await listAllBets(contractId) : [] return { - props: { - contract, - username, - slug: contractSlug, - bets, - }, - + props: { contract, bets }, revalidate: 60, // regenerate after a minute } } @@ -58,16 +52,9 @@ export async function getStaticPaths() { export default function ContractEmbedPage(props: { contract: Contract | null - username: string bets: Bet[] - slug: string }) { - props = usePropz(props, getStaticPropz) ?? { - contract: null, - username: '', - bets: [], - slug: '', - } + props = usePropz(props, getStaticPropz) ?? { contract: null, bets: [] } const contract = useContractWithPreload(props.contract) const { bets } = props diff --git a/web/pages/group/[...slugs]/index.tsx b/web/pages/group/[...slugs]/index.tsx index 3adb01c1..f06247cd 100644 --- a/web/pages/group/[...slugs]/index.tsx +++ b/web/pages/group/[...slugs]/index.tsx @@ -1,7 +1,7 @@ import React, { useState } from 'react' import Link from 'next/link' import { useRouter } from 'next/router' -import { toast, Toaster } from 'react-hot-toast' +import { toast } from 'react-hot-toast' import { Group, GROUP_CHAT_SLUG } from 'common/group' import { Contract, listContractsByGroupSlug } from 'web/lib/firebase/contracts' @@ -48,11 +48,11 @@ import { Spacer } from 'web/components/layout/spacer' import { usePost } from 'web/hooks/use-post' import { useAdmin } from 'web/hooks/use-admin' import { track } from '@amplitude/analytics-browser' -import { GroupNavBar } from 'web/components/nav/group-nav-bar' import { ArrowLeftIcon } from '@heroicons/react/solid' -import { GroupSidebar } from 'web/components/nav/group-sidebar' import { SelectMarketsModal } from 'web/components/contract-select-modal' import { BETTORS } from 'common/user' +import { Page } from 'web/components/page' +import { Tabs } from 'web/components/layout/tabs' export const getStaticProps = fromPropz(getStaticPropz) export async function getStaticPropz(props: { params: { slugs: string[] } }) { @@ -140,10 +140,6 @@ export default function GroupPage(props: { const user = useUser() const isAdmin = useAdmin() const memberIds = useMemberIds(group?.id ?? null) ?? props.memberIds - // Note: Keep in sync with sidebarPages - const [sidebarIndex, setSidebarIndex] = useState( - ['markets', 'leaderboards', 'about'].indexOf(page ?? 'markets') - ) useSaveReferral(user, { defaultReferrerUsername: creator.username, @@ -157,7 +153,7 @@ export default function GroupPage(props: { const isMember = user && memberIds.includes(user.id) const maxLeaderboardSize = 50 - const leaderboardPage = ( + const leaderboardTab = ( <Col> <div className="mt-4 flex flex-col gap-8 px-4 md:flex-row"> <GroupLeaderboard @@ -176,7 +172,7 @@ export default function GroupPage(props: { </Col> ) - const aboutPage = ( + const aboutTab = ( <Col> {(group.aboutPostId != null || isCreator || isAdmin) && ( <GroupAboutPost @@ -196,16 +192,21 @@ export default function GroupPage(props: { </Col> ) - const questionsPage = ( + const questionsTab = ( <> - {/* align the divs to the right */} - <div className={' flex justify-end px-2 pb-2 sm:hidden'}> - <div> - <JoinOrAddQuestionsButtons - group={group} - user={user} - isMember={!!isMember} - /> + <div className={'flex justify-end '}> + <div + className={ + 'flex items-end justify-self-end px-2 md:absolute md:top-0 md:pb-2' + } + > + <div> + <JoinOrAddQuestionsButtons + group={group} + user={user} + isMember={!!isMember} + /> + </div> </div> </div> <ContractSearch @@ -215,92 +216,42 @@ export default function GroupPage(props: { defaultFilter={suggestedFilter} additionalFilter={{ groupSlug: group.slug }} persistPrefix={`group-${group.slug}`} + includeProbSorts /> </> ) - const sidebarPages = [ + const tabs = [ { title: 'Markets', - content: questionsPage, - href: groupPath(group.slug, 'markets'), - key: 'markets', + content: questionsTab, }, { title: 'Leaderboards', - content: leaderboardPage, - href: groupPath(group.slug, 'leaderboards'), - key: 'leaderboards', + content: leaderboardTab, }, { title: 'About', - content: aboutPage, - href: groupPath(group.slug, 'about'), - key: 'about', + content: aboutTab, }, ] - const pageContent = sidebarPages[sidebarIndex].content - const onSidebarClick = (key: string) => { - const index = sidebarPages.findIndex((t) => t.key === key) - setSidebarIndex(index) - // Append the page to the URL, e.g. /group/mexifold/markets - router.replace( - { query: { ...router.query, slugs: [group.slug, key] } }, - undefined, - { shallow: true } - ) - } - - const joinOrAddQuestionsButton = ( - <JoinOrAddQuestionsButtons - group={group} - user={user} - isMember={!!isMember} - /> - ) - return ( - <> - <TopGroupNavBar - group={group} - currentPage={sidebarPages[sidebarIndex].key} - onClick={onSidebarClick} + <Page logoSubheading={group.name}> + <SEO + title={group.name} + description={`Created by ${creator.name}. ${group.about}`} + url={groupPath(group.slug)} /> - <div> - <div - className={ - 'mx-auto w-full pb-[58px] lg:grid lg:grid-cols-12 lg:gap-x-2 lg:pb-0 xl:max-w-7xl xl:gap-x-8' - } - > - <Toaster /> - <GroupSidebar - groupName={group.name} - className="sticky top-0 hidden divide-gray-300 self-start pl-2 lg:col-span-2 lg:flex" - onClick={onSidebarClick} - joinOrAddQuestionsButton={joinOrAddQuestionsButton} - currentKey={sidebarPages[sidebarIndex].key} - /> - - <SEO - title={group.name} - description={`Created by ${creator.name}. ${group.about}`} - url={groupPath(group.slug)} - /> - <main className={'px-2 pt-1 lg:col-span-8 lg:pt-6 xl:col-span-8'}> - {pageContent} - </main> - </div> + <TopGroupNavBar group={group} /> + <div className={'relative p-2 pt-0 md:pt-2'}> + <Tabs className={'mb-2'} tabs={tabs} /> </div> - </> + </Page> ) } -export function TopGroupNavBar(props: { - group: Group - currentPage: string - onClick: (key: string) => void -}) { +export function TopGroupNavBar(props: { group: Group }) { return ( <header className="sticky top-0 z-50 w-full border-b border-gray-200 md:hidden lg:col-span-12"> <div className="flex items-center bg-white px-4"> @@ -317,7 +268,6 @@ export function TopGroupNavBar(props: { </h1> </div> </div> - <GroupNavBar currentPage={props.currentPage} onClick={props.onClick} /> </header> ) } @@ -330,11 +280,13 @@ function JoinOrAddQuestionsButtons(props: { }) { const { group, user, isMember } = props return user && isMember ? ( - <Row className={'w-full self-start pt-4'}> + <Row className={'mb-2 w-full self-start md:mt-2 '}> <AddContractButton group={group} user={user} /> </Row> ) : group.anyoneCanJoin ? ( - <JoinGroupButton group={group} user={user} /> + <div className="mb-2 md:mb-0"> + <JoinGroupButton group={group} user={user} /> + </div> ) : null } @@ -451,7 +403,7 @@ function GroupLeaderboard(props: { return ( <Leaderboard className="max-w-xl" - users={topUsers.map((t) => t.user)} + entries={topUsers.map((t) => t.user)} title={title} columns={[ { header, renderCell: (user) => formatMoney(scoresByUser[user.id]) }, diff --git a/web/pages/groups.tsx b/web/pages/groups.tsx index 1854da34..49d99d18 100644 --- a/web/pages/groups.tsx +++ b/web/pages/groups.tsx @@ -99,6 +99,7 @@ export default function Groups(props: { </div> <Tabs + className="mb-4" currentPageForAnalytics={'groups'} tabs={[ ...(user diff --git a/web/pages/leaderboards.tsx b/web/pages/leaderboards.tsx index 4f1e9437..e663d81c 100644 --- a/web/pages/leaderboards.tsx +++ b/web/pages/leaderboards.tsx @@ -81,7 +81,7 @@ export default function Leaderboards(_props: { <Col className="mx-4 items-center gap-10 lg:flex-row"> <Leaderboard title={`🏅 Top ${BETTORS}`} - users={topTraders} + entries={topTraders} columns={[ { header: 'Total profit', @@ -92,7 +92,7 @@ export default function Leaderboards(_props: { <Leaderboard title="🏅 Top creators" - users={topCreators} + entries={topCreators} columns={[ { header: 'Total bet', @@ -106,7 +106,7 @@ export default function Leaderboards(_props: { <Col className="mx-4 my-10 items-center gap-10 lg:mx-0 lg:w-1/2 lg:flex-row"> <Leaderboard title="🏅 Top followed" - users={topFollowed} + entries={topFollowed} columns={[ { header: 'Total followers', @@ -132,6 +132,7 @@ export default function Leaderboards(_props: { /> <Title text={'Leaderboards'} className={'hidden md:block'} /> <Tabs + className="mb-4" currentPageForAnalytics={'leaderboards'} defaultIndex={1} tabs={[ diff --git a/web/pages/stats.tsx b/web/pages/stats.tsx index 40847470..19fab509 100644 --- a/web/pages/stats.tsx +++ b/web/pages/stats.tsx @@ -26,6 +26,7 @@ export default function Analytics() { return ( <Page> <Tabs + className="mb-4" currentPageForAnalytics={'stats'} tabs={[ { @@ -89,6 +90,7 @@ export function CustomAnalytics(props: Stats) { <Spacer h={4} /> <Tabs + className="mb-4" defaultIndex={1} tabs={[ { @@ -141,6 +143,7 @@ export function CustomAnalytics(props: Stats) { period? </p> <Tabs + className="mb-4" defaultIndex={1} tabs={[ { @@ -198,6 +201,7 @@ export function CustomAnalytics(props: Stats) { <Spacer h={4} /> <Tabs + className="mb-4" defaultIndex={2} tabs={[ { @@ -239,6 +243,7 @@ export function CustomAnalytics(props: Stats) { <Title text="Daily activity" /> <Tabs + className="mb-4" defaultIndex={0} tabs={[ { @@ -293,6 +298,7 @@ export function CustomAnalytics(props: Stats) { <Spacer h={4} /> <Tabs + className="mb-4" defaultIndex={1} tabs={[ { @@ -323,6 +329,7 @@ export function CustomAnalytics(props: Stats) { <Title text="Ratio of Active Users" /> <Tabs + className="mb-4" defaultIndex={1} tabs={[ { @@ -367,6 +374,7 @@ export function CustomAnalytics(props: Stats) { Sum of bet amounts. (Divided by 100 to be more readable.) </p> <Tabs + className="mb-4" defaultIndex={1} tabs={[ { diff --git a/web/pages/tournaments/index.tsx b/web/pages/tournaments/index.tsx index b56e55e6..8378b185 100644 --- a/web/pages/tournaments/index.tsx +++ b/web/pages/tournaments/index.tsx @@ -83,14 +83,14 @@ const tourneys: Tourney[] = [ endTime: toDate('Sep 30, 2022'), groupId: 'fhksfIgqyWf7OxsV9nkM', }, - { - title: 'Manifold F2P Tournament', - blurb: - 'Who can amass the most mana starting from a free-to-play (F2P) account?', - award: 'Poem', - endTime: toDate('Sep 15, 2022'), - groupId: '6rrIja7tVW00lUVwtsYS', - }, + // { + // title: 'Manifold F2P Tournament', + // blurb: + // 'Who can amass the most mana starting from a free-to-play (F2P) account?', + // award: 'Poem', + // endTime: toDate('Sep 15, 2022'), + // groupId: '6rrIja7tVW00lUVwtsYS', + // }, // { // title: 'Cause Exploration Prizes', // blurb: diff --git a/web/posts/post-comments.tsx b/web/posts/post-comments.tsx index d129f807..b98887bb 100644 --- a/web/posts/post-comments.tsx +++ b/web/posts/post-comments.tsx @@ -92,7 +92,7 @@ export function PostCommentInput(props: { return ( <CommentInput - replyToUser={replyToUser} + replyTo={replyToUser} parentCommentId={parentCommentId} onSubmitComment={onSubmitComment} /> diff --git a/yarn.lock b/yarn.lock index 89d43cba..81cf80fa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2476,10 +2476,10 @@ resolved "https://registry.yarnpkg.com/@mdx-js/util/-/util-1.6.22.tgz#219dfd89ae5b97a8801f015323ffa4b62f45718b" integrity sha512-H1rQc1ZOHANWBvPcW+JpGwr+juXSxM8Q8YCkm3GhZd8REu1fHR3z99CErO1p9pkcfcxZnMdIZdIsXkOHY0NilA== -"@next/env@12.2.5": - version "12.2.5" - resolved "https://registry.yarnpkg.com/@next/env/-/env-12.2.5.tgz#d908c57b35262b94db3e431e869b72ac3e1ad3e3" - integrity sha512-vLPLV3cpPGjUPT3PjgRj7e3nio9t6USkuew3JE/jMeon/9Mvp1WyR18v3iwnCuX7eUAm1HmAbJHHLAbcu/EJcw== +"@next/env@12.3.1": + version "12.3.1" + resolved "https://registry.yarnpkg.com/@next/env/-/env-12.3.1.tgz#18266bd92de3b4aa4037b1927aa59e6f11879260" + integrity sha512-9P9THmRFVKGKt9DYqeC2aKIxm8rlvkK38V1P1sRE7qyoPBIs8l9oo79QoSdPtOWfzkbDAVUqvbQGgTMsb8BtJg== "@next/eslint-plugin-next@12.1.6": version "12.1.6" @@ -2488,70 +2488,70 @@ dependencies: glob "7.1.7" -"@next/swc-android-arm-eabi@12.2.5": - version "12.2.5" - resolved "https://registry.yarnpkg.com/@next/swc-android-arm-eabi/-/swc-android-arm-eabi-12.2.5.tgz#903a5479ab4c2705d9c08d080907475f7bacf94d" - integrity sha512-cPWClKxGhgn2dLWnspW+7psl3MoLQUcNqJqOHk2BhNcou9ARDtC0IjQkKe5qcn9qg7I7U83Gp1yh2aesZfZJMA== +"@next/swc-android-arm-eabi@12.3.1": + version "12.3.1" + resolved "https://registry.yarnpkg.com/@next/swc-android-arm-eabi/-/swc-android-arm-eabi-12.3.1.tgz#b15ce8ad376102a3b8c0f3c017dde050a22bb1a3" + integrity sha512-i+BvKA8tB//srVPPQxIQN5lvfROcfv4OB23/L1nXznP+N/TyKL8lql3l7oo2LNhnH66zWhfoemg3Q4VJZSruzQ== -"@next/swc-android-arm64@12.2.5": - version "12.2.5" - resolved "https://registry.yarnpkg.com/@next/swc-android-arm64/-/swc-android-arm64-12.2.5.tgz#2f9a98ec4166c7860510963b31bda1f57a77c792" - integrity sha512-vMj0efliXmC5b7p+wfcQCX0AfU8IypjkzT64GiKJD9PgiA3IILNiGJr1fw2lyUDHkjeWx/5HMlMEpLnTsQslwg== +"@next/swc-android-arm64@12.3.1": + version "12.3.1" + resolved "https://registry.yarnpkg.com/@next/swc-android-arm64/-/swc-android-arm64-12.3.1.tgz#85d205f568a790a137cb3c3f720d961a2436ac9c" + integrity sha512-CmgU2ZNyBP0rkugOOqLnjl3+eRpXBzB/I2sjwcGZ7/Z6RcUJXK5Evz+N0ucOxqE4cZ3gkTeXtSzRrMK2mGYV8Q== -"@next/swc-darwin-arm64@12.2.5": - version "12.2.5" - resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-12.2.5.tgz#31b1c3c659d54be546120c488a1e1bad21c24a1d" - integrity sha512-VOPWbO5EFr6snla/WcxUKtvzGVShfs302TEMOtzYyWni6f9zuOetijJvVh9CCTzInnXAZMtHyNhefijA4HMYLg== +"@next/swc-darwin-arm64@12.3.1": + version "12.3.1" + resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-12.3.1.tgz#b105457d6760a7916b27e46c97cb1a40547114ae" + integrity sha512-hT/EBGNcu0ITiuWDYU9ur57Oa4LybD5DOQp4f22T6zLfpoBMfBibPtR8XktXmOyFHrL/6FC2p9ojdLZhWhvBHg== -"@next/swc-darwin-x64@12.2.5": - version "12.2.5" - resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-12.2.5.tgz#2e44dd82b2b7fef88238d1bc4d3bead5884cedfd" - integrity sha512-5o8bTCgAmtYOgauO/Xd27vW52G2/m3i5PX7MUYePquxXAnX73AAtqA3WgPXBRitEB60plSKZgOTkcpqrsh546A== +"@next/swc-darwin-x64@12.3.1": + version "12.3.1" + resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-12.3.1.tgz#6947b39082271378896b095b6696a7791c6e32b1" + integrity sha512-9S6EVueCVCyGf2vuiLiGEHZCJcPAxglyckTZcEwLdJwozLqN0gtS0Eq0bQlGS3dH49Py/rQYpZ3KVWZ9BUf/WA== -"@next/swc-freebsd-x64@12.2.5": - version "12.2.5" - resolved "https://registry.yarnpkg.com/@next/swc-freebsd-x64/-/swc-freebsd-x64-12.2.5.tgz#e24e75d8c2581bfebc75e4f08f6ddbd116ce9dbd" - integrity sha512-yYUbyup1JnznMtEBRkK4LT56N0lfK5qNTzr6/DEyDw5TbFVwnuy2hhLBzwCBkScFVjpFdfiC6SQAX3FrAZzuuw== +"@next/swc-freebsd-x64@12.3.1": + version "12.3.1" + resolved "https://registry.yarnpkg.com/@next/swc-freebsd-x64/-/swc-freebsd-x64-12.3.1.tgz#2b6c36a4d84aae8b0ea0e0da9bafc696ae27085a" + integrity sha512-qcuUQkaBZWqzM0F1N4AkAh88lLzzpfE6ImOcI1P6YeyJSsBmpBIV8o70zV+Wxpc26yV9vpzb+e5gCyxNjKJg5Q== -"@next/swc-linux-arm-gnueabihf@12.2.5": - version "12.2.5" - resolved "https://registry.yarnpkg.com/@next/swc-linux-arm-gnueabihf/-/swc-linux-arm-gnueabihf-12.2.5.tgz#46d8c514d834d2b5f67086013f0bd5e3081e10b9" - integrity sha512-2ZE2/G921Acks7UopJZVMgKLdm4vN4U0yuzvAMJ6KBavPzqESA2yHJlm85TV/K9gIjKhSk5BVtauIUntFRP8cg== +"@next/swc-linux-arm-gnueabihf@12.3.1": + version "12.3.1" + resolved "https://registry.yarnpkg.com/@next/swc-linux-arm-gnueabihf/-/swc-linux-arm-gnueabihf-12.3.1.tgz#6e421c44285cfedac1f4631d5de330dd60b86298" + integrity sha512-diL9MSYrEI5nY2wc/h/DBewEDUzr/DqBjIgHJ3RUNtETAOB3spMNHvJk2XKUDjnQuluLmFMloet9tpEqU2TT9w== -"@next/swc-linux-arm64-gnu@12.2.5": - version "12.2.5" - resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-12.2.5.tgz#91f725ac217d3a1f4f9f53b553615ba582fd3d9f" - integrity sha512-/I6+PWVlz2wkTdWqhlSYYJ1pWWgUVva6SgX353oqTh8njNQp1SdFQuWDqk8LnM6ulheVfSsgkDzxrDaAQZnzjQ== +"@next/swc-linux-arm64-gnu@12.3.1": + version "12.3.1" + resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-12.3.1.tgz#8863f08a81f422f910af126159d2cbb9552ef717" + integrity sha512-o/xB2nztoaC7jnXU3Q36vGgOolJpsGG8ETNjxM1VAPxRwM7FyGCPHOMk1XavG88QZSQf+1r+POBW0tLxQOJ9DQ== -"@next/swc-linux-arm64-musl@12.2.5": - version "12.2.5" - resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-12.2.5.tgz#e627e8c867920995810250303cd9b8e963598383" - integrity sha512-LPQRelfX6asXyVr59p5sTpx5l+0yh2Vjp/R8Wi4X9pnqcayqT4CUJLiHqCvZuLin3IsFdisJL0rKHMoaZLRfmg== +"@next/swc-linux-arm64-musl@12.3.1": + version "12.3.1" + resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-12.3.1.tgz#0038f07cf0b259d70ae0c80890d826dfc775d9f3" + integrity sha512-2WEasRxJzgAmP43glFNhADpe8zB7kJofhEAVNbDJZANp+H4+wq+/cW1CdDi8DqjkShPEA6/ejJw+xnEyDID2jg== -"@next/swc-linux-x64-gnu@12.2.5": - version "12.2.5" - resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-12.2.5.tgz#83a5e224fbc4d119ef2e0f29d0d79c40cc43887e" - integrity sha512-0szyAo8jMCClkjNK0hknjhmAngUppoRekW6OAezbEYwHXN/VNtsXbfzgYOqjKWxEx3OoAzrT3jLwAF0HdX2MEw== +"@next/swc-linux-x64-gnu@12.3.1": + version "12.3.1" + resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-12.3.1.tgz#c66468f5e8181ffb096c537f0dbfb589baa6a9c1" + integrity sha512-JWEaMyvNrXuM3dyy9Pp5cFPuSSvG82+yABqsWugjWlvfmnlnx9HOQZY23bFq3cNghy5V/t0iPb6cffzRWylgsA== -"@next/swc-linux-x64-musl@12.2.5": - version "12.2.5" - resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-12.2.5.tgz#be700d48471baac1ec2e9539396625584a317e95" - integrity sha512-zg/Y6oBar1yVnW6Il1I/08/2ukWtOG6s3acdJdEyIdsCzyQi4RLxbbhkD/EGQyhqBvd3QrC6ZXQEXighQUAZ0g== +"@next/swc-linux-x64-musl@12.3.1": + version "12.3.1" + resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-12.3.1.tgz#c6269f3e96ac0395bc722ad97ce410ea5101d305" + integrity sha512-xoEWQQ71waWc4BZcOjmatuvPUXKTv6MbIFzpm4LFeCHsg2iwai0ILmNXf81rJR+L1Wb9ifEke2sQpZSPNz1Iyg== -"@next/swc-win32-arm64-msvc@12.2.5": - version "12.2.5" - resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-12.2.5.tgz#a93e958133ad3310373fda33a79aa10af2a0aa97" - integrity sha512-3/90DRNSqeeSRMMEhj4gHHQlLhhKg5SCCoYfE3kBjGpE63EfnblYUqsszGGZ9ekpKL/R4/SGB40iCQr8tR5Jiw== +"@next/swc-win32-arm64-msvc@12.3.1": + version "12.3.1" + resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-12.3.1.tgz#83c639ee969cee36ce247c3abd1d9df97b5ecade" + integrity sha512-hswVFYQYIeGHE2JYaBVtvqmBQ1CppplQbZJS/JgrVI3x2CurNhEkmds/yqvDONfwfbttTtH4+q9Dzf/WVl3Opw== -"@next/swc-win32-ia32-msvc@12.2.5": - version "12.2.5" - resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-12.2.5.tgz#4f5f7ba0a98ff89a883625d4af0125baed8b2e19" - integrity sha512-hGLc0ZRAwnaPL4ulwpp4D2RxmkHQLuI8CFOEEHdzZpS63/hMVzv81g8jzYA0UXbb9pus/iTc3VRbVbAM03SRrw== +"@next/swc-win32-ia32-msvc@12.3.1": + version "12.3.1" + resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-12.3.1.tgz#52995748b92aa8ad053440301bc2c0d9fbcf27c2" + integrity sha512-Kny5JBehkTbKPmqulr5i+iKntO5YMP+bVM8Hf8UAmjSMVo3wehyLVc9IZkNmcbxi+vwETnQvJaT5ynYBkJ9dWA== -"@next/swc-win32-x64-msvc@12.2.5": - version "12.2.5" - resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-12.2.5.tgz#20fed129b04a0d3f632c6d0de135345bb623b1e4" - integrity sha512-7h5/ahY7NeaO2xygqVrSG/Y8Vs4cdjxIjowTZ5W6CKoTKn7tmnuxlUc2h74x06FKmbhAd9agOjr/AOKyxYYm9Q== +"@next/swc-win32-x64-msvc@12.3.1": + version "12.3.1" + resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-12.3.1.tgz#27d71a95247a9eaee03d47adee7e3bd594514136" + integrity sha512-W1ijvzzg+kPEX6LAc+50EYYSEo0FVu7dmTE+t+DM4iOLqgGHoW9uYSz9wCVdkXOEEMP9xhXfGpcSxsfDucyPkA== "@nivo/annotations@0.74.0": version "0.74.0" @@ -2933,10 +2933,10 @@ "@svgr/plugin-jsx" "^6.2.1" "@svgr/plugin-svgo" "^6.2.0" -"@swc/helpers@0.4.3": - version "0.4.3" - resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.4.3.tgz#16593dfc248c53b699d4b5026040f88ddb497012" - integrity sha512-6JrF+fdUK2zbGpJIlN7G3v966PQjyx/dPt1T9km2wj+EUBqgrxCk3uX4Kct16MIm9gGxfKRcfax2hVf5jvlTzA== +"@swc/helpers@0.4.11": + version "0.4.11" + resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.4.11.tgz#db23a376761b3d31c26502122f349a21b592c8de" + integrity sha512-rEUrBSGIoSFuYxwBYtlUFMlE2CwGhmW+w9355/5oduSw8e5h2+Tj4UrAGNNgP9915++wj5vkQo0UuOBqOAq4nw== dependencies: tslib "^2.4.0" @@ -4545,6 +4545,11 @@ caniuse-lite@^1.0.30001230, caniuse-lite@^1.0.30001332: resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001341.tgz#59590c8ffa8b5939cf4161f00827b8873ad72498" integrity sha512-2SodVrFFtvGENGCv0ChVJIDQ0KPaS1cg7/qtfMaICgeMolDdo/Z2OD32F0Aq9yl6F4YFwGPBS5AaPqNYiW4PoA== +caniuse-lite@^1.0.30001406: + version "1.0.30001409" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001409.tgz#6135da9dcab34cd9761d9cdb12a68e6740c5e96e" + integrity sha512-V0mnJ5dwarmhYv8/MzhJ//aW68UpvnQBXv8lJ2QUsvn2pHcmAuNtu8hQEDz37XnA1iE+lRR9CIfGWWpgJ5QedQ== + ccount@^1.0.0, ccount@^1.0.3: version "1.1.0" resolved "https://registry.yarnpkg.com/ccount/-/ccount-1.1.0.tgz#246687debb6014735131be8abab2d93898f8d043" @@ -8637,31 +8642,31 @@ next-sitemap@^2.5.14: "@corex/deepmerge" "^2.6.148" minimist "^1.2.6" -next@12.2.5: - version "12.2.5" - resolved "https://registry.yarnpkg.com/next/-/next-12.2.5.tgz#14fb5975e8841fad09553b8ef41fe1393602b717" - integrity sha512-tBdjqX5XC/oFs/6gxrZhjmiq90YWizUYU6qOWAfat7zJwrwapJ+BYgX2PmiacunXMaRpeVT4vz5MSPSLgNkrpA== +next@12.3.1: + version "12.3.1" + resolved "https://registry.yarnpkg.com/next/-/next-12.3.1.tgz#127b825ad2207faf869b33393ec8c75fe61e50f1" + integrity sha512-l7bvmSeIwX5lp07WtIiP9u2ytZMv7jIeB8iacR28PuUEFG5j0HGAPnMqyG5kbZNBG2H7tRsrQ4HCjuMOPnANZw== dependencies: - "@next/env" "12.2.5" - "@swc/helpers" "0.4.3" - caniuse-lite "^1.0.30001332" + "@next/env" "12.3.1" + "@swc/helpers" "0.4.11" + caniuse-lite "^1.0.30001406" postcss "8.4.14" - styled-jsx "5.0.4" + styled-jsx "5.0.7" use-sync-external-store "1.2.0" optionalDependencies: - "@next/swc-android-arm-eabi" "12.2.5" - "@next/swc-android-arm64" "12.2.5" - "@next/swc-darwin-arm64" "12.2.5" - "@next/swc-darwin-x64" "12.2.5" - "@next/swc-freebsd-x64" "12.2.5" - "@next/swc-linux-arm-gnueabihf" "12.2.5" - "@next/swc-linux-arm64-gnu" "12.2.5" - "@next/swc-linux-arm64-musl" "12.2.5" - "@next/swc-linux-x64-gnu" "12.2.5" - "@next/swc-linux-x64-musl" "12.2.5" - "@next/swc-win32-arm64-msvc" "12.2.5" - "@next/swc-win32-ia32-msvc" "12.2.5" - "@next/swc-win32-x64-msvc" "12.2.5" + "@next/swc-android-arm-eabi" "12.3.1" + "@next/swc-android-arm64" "12.3.1" + "@next/swc-darwin-arm64" "12.3.1" + "@next/swc-darwin-x64" "12.3.1" + "@next/swc-freebsd-x64" "12.3.1" + "@next/swc-linux-arm-gnueabihf" "12.3.1" + "@next/swc-linux-arm64-gnu" "12.3.1" + "@next/swc-linux-arm64-musl" "12.3.1" + "@next/swc-linux-x64-gnu" "12.3.1" + "@next/swc-linux-x64-musl" "12.3.1" + "@next/swc-win32-arm64-msvc" "12.3.1" + "@next/swc-win32-ia32-msvc" "12.3.1" + "@next/swc-win32-x64-msvc" "12.3.1" no-case@^3.0.4: version "3.0.4" @@ -11267,10 +11272,10 @@ style-to-object@0.3.0, style-to-object@^0.3.0: dependencies: inline-style-parser "0.1.1" -styled-jsx@5.0.4: - version "5.0.4" - resolved "https://registry.yarnpkg.com/styled-jsx/-/styled-jsx-5.0.4.tgz#5b1bd0b9ab44caae3dd1361295559706e044aa53" - integrity sha512-sDFWLbg4zR+UkNzfk5lPilyIgtpddfxXEULxhujorr5jtePTUqiPDc5BC0v1NRqTr/WaFBGQQUoYToGlF4B2KQ== +styled-jsx@5.0.7: + version "5.0.7" + resolved "https://registry.yarnpkg.com/styled-jsx/-/styled-jsx-5.0.7.tgz#be44afc53771b983769ac654d355ca8d019dff48" + integrity sha512-b3sUzamS086YLRuvnaDigdAewz1/EFYlHpYBP5mZovKEdQQOIIYq8lApylub3HHZ6xFjV051kkGU7cudJmrXEA== stylehacks@^5.1.0: version "5.1.0"