From c7f29af2ee5e31903e0de082d569af9493e83af8 Mon Sep 17 00:00:00 2001 From: Marshall Polaris Date: Tue, 20 Sep 2022 22:07:40 -0700 Subject: [PATCH 01/23] Clean up some stuff in `AnswersPanel` (#902) * Tidy up messy markup on FR answers panel * Clean up obsolete feed-related answer stuff * Slight fixup per James feedback --- web/components/answers/answers-panel.tsx | 127 ++++++++--------------- 1 file changed, 45 insertions(+), 82 deletions(-) diff --git a/web/components/answers/answers-panel.tsx b/web/components/answers/answers-panel.tsx index 41b7f0f9..a1cef4c3 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' @@ -56,6 +55,11 @@ export function AnswersPanel(props: { ), ] + const answerItems = sortBy( + losingAnswers.length > 0 ? losingAnswers : sortedAnswers, + (answer) => -getOutcomeProbability(contract, answer.id) + ) + const user = useUser() const [resolveOption, setResolveOption] = useState< @@ -67,12 +71,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 }) @@ -123,28 +121,26 @@ export function AnswersPanel(props: { ))} {!resolveOption && ( -
-
- {answerItems.map((item) => ( -
-
- -
-
- ))} - - {hasZeroBetAnswers && !showAllAnswers && ( - - )} - -
-
+ + {answerItems.map((item) => ( + + ))} + {hasZeroBetAnswers && !showAllAnswers && ( + + )} + )} {answers.length <= 1 && ( @@ -175,35 +171,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 +182,7 @@ function OpenAnswer(props: { const [open, setOpen] = useState(false) return ( - + -
- -
+
answered
- - - - - -
- - {probPercent} - - setOpen(true)} - /> -
+ + + + {probPercent} + + setOpen(true)} + /> From a10605e74c26580b6a5df7160939e913645c74eb Mon Sep 17 00:00:00 2001 From: Marshall Polaris Date: Wed, 21 Sep 2022 00:02:10 -0700 Subject: [PATCH 02/23] More work on contract page and tabs (#912) * Consolidate comment thread component code * Move `visibleBets` work down into bets tab * Remove unnecessary cruft from contract page props * Don't load all comments in contract page static props anymore * Tidy up props a bit * Memoize bets tab * Memoize recommended contracts widget --- .../contract/contract-leaderboard.tsx | 12 +- web/components/contract/contract-tabs.tsx | 194 +++++++----------- web/pages/[username]/[contractSlug].tsx | 114 ++++------ web/pages/embed/[username]/[contractSlug].tsx | 19 +- 4 files changed, 125 insertions(+), 214 deletions(-) diff --git a/web/components/contract/contract-leaderboard.tsx b/web/components/contract/contract-leaderboard.tsx index 4d25ffa4..6cd2ae62 100644 --- a/web/components/contract/contract-leaderboard.tsx +++ b/web/components/contract/contract-leaderboard.tsx @@ -1,10 +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 { useComments } from 'web/hooks/use-comments' import { listUsers, User } from 'web/lib/firebase/users' import { FeedBet } from '../feed/feed-bets' import { FeedComment } from '../feed/feed-comments' @@ -61,12 +61,10 @@ export function ContractLeaderboard(props: { ) : null } -export function ContractTopTrades(props: { - contract: Contract - bets: Bet[] - comments: ContractComment[] -}) { - const { contract, bets, comments } = props +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 commentsById = keyBy(comments, 'id') const betsById = keyBy(bets, 'id') diff --git a/web/components/contract/contract-tabs.tsx b/web/components/contract/contract-tabs.tsx index 245a8d7d..008eb584 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 = (
@@ -61,15 +53,11 @@ export function ContractTabs(props: { tabs={[ { title: 'Comments', - content: ( - - ), + content: , }, { title: capitalize(PAST_BETS), - content: ( - - ), + content: , }, ...(!user || !userBets?.length ? [] @@ -86,46 +74,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 + } 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 ( <> - + {sortedAnswers.map((answer) => ( +
+
+ ))}
General Comments
- - comment.answerOutcome === undefined && - comment.betId === undefined - )} - tips={tips} - /> + + {generalComments.map((comment) => ( + + ))} ) } else { + const commentsByParent = groupBy(comments, (c) => c.replyToCommentId ?? '_') + const topLevelComments = commentsByParent['_'] ?? [] return ( - + <> + + {sortBy(topLevelComments, (c) => -c.createdTime).map((parent) => ( + 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 +162,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 +174,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 +216,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 ( - <> - - {topLevelComments.map((parent) => ( - 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) => ( -
-
- ))} - - ) -} +}) 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 @@ -109,9 +85,7 @@ export default function ContractPage(props: { return } - return ( - - ) + return } // 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[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 = return ( - + }> {showConfetti && ( )} @@ -228,7 +199,7 @@ export function ContractPageContent( )} @@ -271,22 +242,13 @@ export function ContractPageContent( <>
- +
)} - + {!user ? ( @@ -307,26 +269,28 @@ export function ContractPageContent( ) } -function RecommendedContractsWidget(props: { contract: Contract }) { - const { contract } = props - const user = useUser() - const [recommendations, setRecommendations] = useState([]) - 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([]) + 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 ( + + + <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/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 From 73fad2e34b3000556d80e86da236cf93daad280d Mon Sep 17 00:00:00 2001 From: Austin Chen <akrolsmir@gmail.com> Date: Wed, 21 Sep 2022 15:31:45 -0400 Subject: [PATCH 03/23] Remove F2P Tournament --- web/pages/tournaments/index.tsx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) 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: From 24766740c585ab77dbda76f04e83c58f926b5858 Mon Sep 17 00:00:00 2001 From: ingawei <46611122+ingawei@users.noreply.github.com> Date: Wed, 21 Sep 2022 16:48:32 -0500 Subject: [PATCH 04/23] cleaning up search bar for mobile (#916) * cleaning up search bar for mobile --- web/components/contract-search.tsx | 131 +++++++++++++++++++++++------ 1 file changed, 107 insertions(+), 24 deletions(-) diff --git a/web/components/contract-search.tsx b/web/components/contract-search.tsx index 3d25dcdd..7ab28cbb 100644 --- a/web/components/contract-search.tsx +++ b/web/components/contract-search.tsx @@ -9,7 +9,14 @@ import { } from './contract/contracts-grid' import { ShowTime } from './contract/contract-details' import { Row } from './layout/row' -import { useEffect, useLayoutEffect, useRef, useMemo, ReactNode } from 'react' +import { + useEffect, + useLayoutEffect, + useRef, + useMemo, + ReactNode, + useState, +} from 'react' import { IS_PRIVATE_MANIFOLD } from 'common/envs/constants' import { useFollows } from 'web/hooks/use-follows' import { @@ -32,6 +39,11 @@ import { searchClient, searchIndexName, } from 'web/lib/service/algolia' +import { useIsMobile } from 'web/hooks/use-is-mobile' +import { AdjustmentsIcon } from '@heroicons/react/solid' +import { Button } from './button' +import { Modal } from './layout/modal' +import { Title } from './title' export const SORTS = [ { label: 'Newest', value: 'newest' }, @@ -270,6 +282,8 @@ function ContractSearchControls(props: { } ) + const isMobile = useIsMobile() + const sortKey = `${persistPrefix}-search-sort` const savedSort = safeLocalStorage()?.getItem(sortKey) @@ -415,30 +429,31 @@ function ContractSearchControls(props: { className="input input-bordered w-full" autoFocus={autoFocus} /> - {!query && ( - <select - className="select select-bordered" - value={filter} - onChange={(e) => selectFilter(e.target.value as filter)} - > - <option value="open">Open</option> - <option value="closed">Closed</option> - <option value="resolved">Resolved</option> - <option value="all">All</option> - </select> + {!isMobile && ( + <SearchFilters + filter={filter} + selectFilter={selectFilter} + hideOrderSelector={hideOrderSelector} + selectSort={selectSort} + sort={sort} + className={'flex flex-row gap-2'} + /> )} - {!hideOrderSelector && !query && ( - <select - className="select select-bordered" - value={sort} - onChange={(e) => selectSort(e.target.value as Sort)} - > - {SORTS.map((option) => ( - <option key={option.value} value={option.value}> - {option.label} - </option> - ))} - </select> + {isMobile && ( + <> + <MobileSearchBar + children={ + <SearchFilters + filter={filter} + selectFilter={selectFilter} + hideOrderSelector={hideOrderSelector} + selectSort={selectSort} + sort={sort} + className={'flex flex-col gap-4'} + /> + } + /> + </> )} </Row> @@ -481,3 +496,71 @@ function ContractSearchControls(props: { </Col> ) } + +export function SearchFilters(props: { + filter: string + selectFilter: (newFilter: filter) => void + hideOrderSelector: boolean | undefined + selectSort: (newSort: Sort) => void + sort: string + className?: string +}) { + const { + filter, + selectFilter, + hideOrderSelector, + selectSort, + sort, + className, + } = props + return ( + <div className={className}> + <select + className="select select-bordered" + value={filter} + onChange={(e) => selectFilter(e.target.value as filter)} + > + <option value="open">Open</option> + <option value="closed">Closed</option> + <option value="resolved">Resolved</option> + <option value="all">All</option> + </select> + {!hideOrderSelector && ( + <select + className="select select-bordered" + value={sort} + onChange={(e) => selectSort(e.target.value as Sort)} + > + {SORTS.map((option) => ( + <option key={option.value} value={option.value}> + {option.label} + </option> + ))} + </select> + )} + </div> + ) +} + +export function MobileSearchBar(props: { children: ReactNode }) { + const { children } = props + const [openFilters, setOpenFilters] = useState(false) + return ( + <> + <Button color="gray-white" onClick={() => setOpenFilters(true)}> + <AdjustmentsIcon className="my-auto h-7" /> + </Button> + <Modal + open={openFilters} + setOpen={setOpenFilters} + position="top" + className="rounded-lg bg-white px-4 pb-4" + > + <Col> + <Title text="Filter Markets" /> + {children} + </Col> + </Modal> + </> + ) +} From d922900bda2e7ea804d8a3daa28cc828b15f9278 Mon Sep 17 00:00:00 2001 From: Austin Chen <akrolsmir@gmail.com> Date: Wed, 21 Sep 2022 18:25:54 -0400 Subject: [PATCH 05/23] Increase tip size to M$10 --- web/components/tipper.tsx | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) 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 From b4a59cfb212dc99d952800e4c18bd2151aa168d5 Mon Sep 17 00:00:00 2001 From: FRC <pico2x@gmail.com> Date: Wed, 21 Sep 2022 23:27:49 +0100 Subject: [PATCH 06/23] Bring back tabs in groups (#919) --- functions/src/utils.ts | 1 - web/components/nav/group-nav-bar.tsx | 94 --------------------- web/components/nav/group-sidebar.tsx | 82 ------------------ web/components/nav/sidebar.tsx | 17 +++- web/components/page.tsx | 14 +++- web/pages/group/[...slugs]/index.tsx | 119 ++++++++------------------- 6 files changed, 60 insertions(+), 267 deletions(-) delete mode 100644 web/components/nav/group-nav-bar.tsx delete mode 100644 web/components/nav/group-sidebar.tsx diff --git a/functions/src/utils.ts b/functions/src/utils.ts index 6bb8349a..eb5fa8f8 100644 --- a/functions/src/utils.ts +++ b/functions/src/utils.ts @@ -1,5 +1,4 @@ import * as admin from 'firebase-admin' -import fetch from 'node-fetch' import { chunk } from 'lodash' import { Contract } from '../../common/contract' 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/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/pages/group/[...slugs]/index.tsx b/web/pages/group/[...slugs]/index.tsx index 3adb01c1..cf531db0 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 @@ -219,88 +220,37 @@ export default function GroupPage(props: { </> ) - 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 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 +267,6 @@ export function TopGroupNavBar(props: { </h1> </div> </div> - <GroupNavBar currentPage={props.currentPage} onClick={props.onClick} /> </header> ) } @@ -330,7 +279,7 @@ function JoinOrAddQuestionsButtons(props: { }) { const { group, user, isMember } = props return user && isMember ? ( - <Row className={'w-full self-start pt-4'}> + <Row className={'w-full self-start md:mt-2 '}> <AddContractButton group={group} user={user} /> </Row> ) : group.anyoneCanJoin ? ( From b875ac563d08b060fe731ee16f1ac7f17ac15b7d Mon Sep 17 00:00:00 2001 From: Pico2x <pico2x@gmail.com> Date: Wed, 21 Sep 2022 19:14:05 -0400 Subject: [PATCH 07/23] Revert "Bring back tabs in groups (#919)" This reverts commit b4a59cfb212dc99d952800e4c18bd2151aa168d5. --- functions/src/utils.ts | 1 + web/components/nav/group-nav-bar.tsx | 94 +++++++++++++++++++++ web/components/nav/group-sidebar.tsx | 82 ++++++++++++++++++ web/components/nav/sidebar.tsx | 17 +--- web/components/page.tsx | 14 +--- web/pages/group/[...slugs]/index.tsx | 119 +++++++++++++++++++-------- 6 files changed, 267 insertions(+), 60 deletions(-) create mode 100644 web/components/nav/group-nav-bar.tsx create mode 100644 web/components/nav/group-sidebar.tsx diff --git a/functions/src/utils.ts b/functions/src/utils.ts index eb5fa8f8..6bb8349a 100644 --- a/functions/src/utils.ts +++ b/functions/src/utils.ts @@ -1,4 +1,5 @@ import * as admin from 'firebase-admin' +import fetch from 'node-fetch' import { chunk } from 'lodash' import { Contract } from '../../common/contract' diff --git a/web/components/nav/group-nav-bar.tsx b/web/components/nav/group-nav-bar.tsx new file mode 100644 index 00000000..9ea3f5a4 --- /dev/null +++ b/web/components/nav/group-nav-bar.tsx @@ -0,0 +1,94 @@ +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 new file mode 100644 index 00000000..a68064e0 --- /dev/null +++ b/web/components/nav/group-sidebar.tsx @@ -0,0 +1,82 @@ +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 b0a9862b..45347774 100644 --- a/web/components/nav/sidebar.tsx +++ b/web/components/nav/sidebar.tsx @@ -26,14 +26,9 @@ 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 - logoSubheading?: string -}) { - const { className, logoSubheading } = props +export default function Sidebar(props: { className?: string }) { + const { className } = props const router = useRouter() const currentPage = router.pathname @@ -56,13 +51,7 @@ export default function Sidebar(props: { aria-label="Sidebar" className={clsx('flex max-h-[100vh] flex-col', className)} > - <ManifoldLogo className="pt-6" twoLine /> - {logoSubheading && ( - <Row className="pl-2 text-2xl text-indigo-700 sm:mt-3"> - {logoSubheading} - </Row> - )} - <Spacer h={6} /> + <ManifoldLogo className="py-6" twoLine /> {!user && <SignInButton className="mb-4" />} diff --git a/web/components/page.tsx b/web/components/page.tsx index f72db80e..9b26e9f8 100644 --- a/web/components/page.tsx +++ b/web/components/page.tsx @@ -9,15 +9,8 @@ export function Page(props: { className?: string rightSidebarClassName?: string children?: ReactNode - logoSubheading?: string }) { - const { - children, - rightSidebar, - className, - rightSidebarClassName, - logoSubheading, - } = props + const { children, rightSidebar, className, rightSidebarClassName } = props const bottomBarPadding = 'pb-[58px] lg:pb-0 ' return ( @@ -30,10 +23,7 @@ export function Page(props: { )} > <Toaster /> - <Sidebar - logoSubheading={logoSubheading} - className="sticky top-0 hidden divide-gray-300 self-start pl-2 lg:col-span-2 lg:flex" - /> + <Sidebar 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/pages/group/[...slugs]/index.tsx b/web/pages/group/[...slugs]/index.tsx index cf531db0..3adb01c1 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 } from 'react-hot-toast' +import { toast, Toaster } 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,6 +140,10 @@ 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, @@ -153,7 +157,7 @@ export default function GroupPage(props: { const isMember = user && memberIds.includes(user.id) const maxLeaderboardSize = 50 - const leaderboardTab = ( + const leaderboardPage = ( <Col> <div className="mt-4 flex flex-col gap-8 px-4 md:flex-row"> <GroupLeaderboard @@ -172,7 +176,7 @@ export default function GroupPage(props: { </Col> ) - const aboutTab = ( + const aboutPage = ( <Col> {(group.aboutPostId != null || isCreator || isAdmin) && ( <GroupAboutPost @@ -192,21 +196,16 @@ export default function GroupPage(props: { </Col> ) - const questionsTab = ( + const questionsPage = ( <> - <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> + {/* 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> </div> <ContractSearch @@ -220,37 +219,88 @@ export default function GroupPage(props: { </> ) - const tabs = [ + const sidebarPages = [ { title: 'Markets', - content: questionsTab, + content: questionsPage, + href: groupPath(group.slug, 'markets'), + key: 'markets', }, { title: 'Leaderboards', - content: leaderboardTab, + content: leaderboardPage, + href: groupPath(group.slug, 'leaderboards'), + key: 'leaderboards', }, { title: 'About', - content: aboutTab, + content: aboutPage, + href: groupPath(group.slug, 'about'), + key: 'about', }, ] + 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 ( - <Page logoSubheading={group.name}> - <SEO - title={group.name} - description={`Created by ${creator.name}. ${group.about}`} - url={groupPath(group.slug)} + <> + <TopGroupNavBar + group={group} + currentPage={sidebarPages[sidebarIndex].key} + onClick={onSidebarClick} /> - <TopGroupNavBar group={group} /> - <div className={'relative p-2 pt-0 md:pt-2'}> - <Tabs tabs={tabs} /> + <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> </div> - </Page> + </> ) } -export function TopGroupNavBar(props: { group: Group }) { +export function TopGroupNavBar(props: { + group: Group + currentPage: string + onClick: (key: string) => void +}) { 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"> @@ -267,6 +317,7 @@ export function TopGroupNavBar(props: { group: Group }) { </h1> </div> </div> + <GroupNavBar currentPage={props.currentPage} onClick={props.onClick} /> </header> ) } @@ -279,7 +330,7 @@ function JoinOrAddQuestionsButtons(props: { }) { const { group, user, isMember } = props return user && isMember ? ( - <Row className={'w-full self-start md:mt-2 '}> + <Row className={'w-full self-start pt-4'}> <AddContractButton group={group} user={user} /> </Row> ) : group.anyoneCanJoin ? ( From 7988fdde60191ae65695081ec92c8bde56ab79e7 Mon Sep 17 00:00:00 2001 From: ingawei <46611122+ingawei@users.noreply.github.com> Date: Wed, 21 Sep 2022 18:49:20 -0500 Subject: [PATCH 08/23] simplify binary graphs (#921) --- web/components/contract/contract-prob-graph.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) 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 }} From e9ab234d610a41fd1dcad807bd94263e76fcd012 Mon Sep 17 00:00:00 2001 From: Sinclair Chen <abc.sinclair@gmail.com> Date: Wed, 21 Sep 2022 17:49:32 -0700 Subject: [PATCH 09/23] copy: manifold dollars -> mana --- docs/docs/faq.md | 6 +----- web/components/add-funds-button.tsx | 4 ++-- web/pages/add-funds.tsx | 10 +++++----- 3 files changed, 8 insertions(+), 12 deletions(-) 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 }) { <div className="modal"> <div className="modal-box"> - <div className="mb-6 text-xl">Get Manifold Dollars</div> + <div className="mb-6 text-xl">Get Mana</div> <div className="mb-6 text-gray-500"> - Use Manifold Dollars to trade in your favorite markets. <br /> (Not + Buy mana (M$) to trade in your favorite markets. <br /> (Not redeemable for cash.) </div> 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> From 9ff2b6274017b7a5db4a256b85f31d54cd6e7496 Mon Sep 17 00:00:00 2001 From: Austin Chen <akrolsmir@gmail.com> Date: Wed, 21 Sep 2022 23:10:22 -0400 Subject: [PATCH 10/23] Remove console log --- web/components/onboarding/welcome.tsx | 2 -- 1 file changed, 2 deletions(-) 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 }) } From c15285aa6412d68e66520fdc2aa792e001a0b04d Mon Sep 17 00:00:00 2001 From: mantikoros <sgrugett@gmail.com> Date: Thu, 22 Sep 2022 00:20:02 -0400 Subject: [PATCH 11/23] pare down sorts; only show high/low prob on groups --- web/components/contract-search.tsx | 22 +++++++++++++++++----- web/pages/group/[...slugs]/index.tsx | 1 + 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/web/components/contract-search.tsx b/web/components/contract-search.tsx index 7ab28cbb..4b14d4a7 100644 --- a/web/components/contract-search.tsx +++ b/web/components/contract-search.tsx @@ -49,17 +49,15 @@ export const SORTS = [ { label: 'Newest', value: 'newest' }, { label: 'Trending', value: 'score' }, { label: `Most traded`, value: 'most-traded' }, - { label: '24h volume', value: '24-hour-vol' }, - { label: '24h change', value: 'prob-change-day' }, { label: 'Last updated', value: 'last-updated' }, - { label: 'Subsidy', value: 'liquidity' }, - { label: 'Close date', value: 'close-date' }, + { label: 'Closing soon', value: 'close-date' }, { label: 'Resolve date', value: 'resolve-date' }, { label: 'Highest %', value: 'prob-descending' }, { label: 'Lowest %', value: 'prob-ascending' }, ] as const export type Sort = typeof SORTS[number]['value'] +export const PROB_SORTS = ['prob-descending', 'prob-ascending'] type filter = 'personal' | 'open' | 'closed' | 'resolved' | 'all' @@ -95,6 +93,7 @@ export function ContractSearch(props: { persistPrefix?: string useQueryUrlParam?: boolean isWholePage?: boolean + includeProbSorts?: boolean noControls?: boolean maxResults?: number renderContracts?: ( @@ -116,6 +115,7 @@ export function ContractSearch(props: { headerClassName, persistPrefix, useQueryUrlParam, + includeProbSorts, isWholePage, noControls, maxResults, @@ -221,6 +221,7 @@ export function ContractSearch(props: { persistPrefix={persistPrefix} hideOrderSelector={hideOrderSelector} useQueryUrlParam={useQueryUrlParam} + includeProbSorts={includeProbSorts} user={user} onSearchParametersChanged={onSearchParametersChanged} noControls={noControls} @@ -250,6 +251,7 @@ function ContractSearchControls(props: { additionalFilter?: AdditionalFilter persistPrefix?: string hideOrderSelector?: boolean + includeProbSorts?: boolean onSearchParametersChanged: (params: SearchParameters) => void useQueryUrlParam?: boolean user?: User | null @@ -269,6 +271,7 @@ function ContractSearchControls(props: { user, noControls, autoFocus, + includeProbSorts, } = props const router = useRouter() @@ -437,6 +440,7 @@ function ContractSearchControls(props: { selectSort={selectSort} sort={sort} className={'flex flex-row gap-2'} + includeProbSorts={includeProbSorts} /> )} {isMobile && ( @@ -450,6 +454,7 @@ function ContractSearchControls(props: { selectSort={selectSort} sort={sort} className={'flex flex-col gap-4'} + includeProbSorts={includeProbSorts} /> } /> @@ -504,6 +509,7 @@ export function SearchFilters(props: { selectSort: (newSort: Sort) => void sort: string className?: string + includeProbSorts?: boolean }) { const { filter, @@ -512,7 +518,13 @@ export function SearchFilters(props: { selectSort, sort, className, + includeProbSorts, } = props + + const sorts = includeProbSorts + ? SORTS + : SORTS.filter((sort) => !PROB_SORTS.includes(sort.value)) + return ( <div className={className}> <select @@ -531,7 +543,7 @@ export function SearchFilters(props: { value={sort} onChange={(e) => selectSort(e.target.value as Sort)} > - {SORTS.map((option) => ( + {sorts.map((option) => ( <option key={option.value} value={option.value}> {option.label} </option> diff --git a/web/pages/group/[...slugs]/index.tsx b/web/pages/group/[...slugs]/index.tsx index 3adb01c1..a486e132 100644 --- a/web/pages/group/[...slugs]/index.tsx +++ b/web/pages/group/[...slugs]/index.tsx @@ -215,6 +215,7 @@ export default function GroupPage(props: { defaultFilter={suggestedFilter} additionalFilter={{ groupSlug: group.slug }} persistPrefix={`group-${group.slug}`} + includeProbSorts /> </> ) From 4412d0195cbf9b91d9fab20f54caae6477a3c03a Mon Sep 17 00:00:00 2001 From: Sinclair Chen <abc.sinclair@gmail.com> Date: Thu, 22 Sep 2022 08:53:55 -0700 Subject: [PATCH 12/23] Add tooltips to market header icons (#924) --- .../contract/extra-contract-actions-row.tsx | 42 +++---- .../contract/like-market-button.tsx | 66 +++++----- web/components/follow-market-button.tsx | 118 ++++++++++-------- 3 files changed, 119 insertions(+), 107 deletions(-) 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/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> ) } From a5e293c010f32f54b923f01b47265149d1cc3310 Mon Sep 17 00:00:00 2001 From: FRC <pico2x@gmail.com> Date: Thu, 22 Sep 2022 12:12:53 -0400 Subject: [PATCH 13/23] Bring back tabs in groups (#923) --- web/components/contract/contract-tabs.tsx | 1 + web/components/following-button.tsx | 1 + web/components/layout/tabs.tsx | 2 +- web/components/nav/group-nav-bar.tsx | 94 ----------------- web/components/nav/group-sidebar.tsx | 82 --------------- web/components/nav/sidebar.tsx | 17 ++- web/components/page.tsx | 14 ++- web/components/referrals-button.tsx | 1 + web/pages/challenges/index.tsx | 2 +- web/pages/group/[...slugs]/index.tsx | 123 +++++++--------------- web/pages/groups.tsx | 1 + web/pages/leaderboards.tsx | 1 + web/pages/stats.tsx | 8 ++ 13 files changed, 78 insertions(+), 269 deletions(-) delete mode 100644 web/components/nav/group-nav-bar.tsx delete mode 100644 web/components/nav/group-sidebar.tsx diff --git a/web/components/contract/contract-tabs.tsx b/web/components/contract/contract-tabs.tsx index 008eb584..17471796 100644 --- a/web/components/contract/contract-tabs.tsx +++ b/web/components/contract/contract-tabs.tsx @@ -49,6 +49,7 @@ export function ContractTabs(props: { contract: Contract; bets: Bet[] }) { return ( <Tabs + className="mb-4" currentPageForAnalytics={'contract'} tabs={[ { 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/layout/tabs.tsx b/web/components/layout/tabs.tsx index 3d72b13c..45e7e297 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/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/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/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/group/[...slugs]/index.tsx b/web/pages/group/[...slugs]/index.tsx index a486e132..779677c4 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 @@ -220,88 +221,37 @@ export default function GroupPage(props: { </> ) - 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"> @@ -318,7 +268,6 @@ export function TopGroupNavBar(props: { </h1> </div> </div> - <GroupNavBar currentPage={props.currentPage} onClick={props.onClick} /> </header> ) } @@ -331,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 } 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..623d21c8 100644 --- a/web/pages/leaderboards.tsx +++ b/web/pages/leaderboards.tsx @@ -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={[ { From 06db5515f6ab7b2795d3ece211d2e2c73b8e901a Mon Sep 17 00:00:00 2001 From: mantikoros <sgrugett@gmail.com> Date: Thu, 22 Sep 2022 14:01:37 -0400 Subject: [PATCH 14/23] add qr code to share dialog --- web/components/contract/share-modal.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/web/components/contract/share-modal.tsx b/web/components/contract/share-modal.tsx index e1eb26eb..88cdfb8b 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,7 @@ 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" @@ -69,6 +71,7 @@ export function ShareModal(props: { {linkIcon} Copy link </Button> + <Row className="z-0 flex-wrap justify-center gap-4 self-center"> <TweetButton className="self-start" From 0c0e7b558271c7d4551eb4b4361afc48b9aa1803 Mon Sep 17 00:00:00 2001 From: mantikoros <mantikoros@users.noreply.github.com> Date: Thu, 22 Sep 2022 18:02:17 +0000 Subject: [PATCH 15/23] Auto-prettification --- web/components/contract/share-modal.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/web/components/contract/share-modal.tsx b/web/components/contract/share-modal.tsx index 88cdfb8b..72c7aba3 100644 --- a/web/components/contract/share-modal.tsx +++ b/web/components/contract/share-modal.tsx @@ -55,7 +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} /> + <QRCode + url={shareUrl} + className="self-center" + width={150} + height={150} + /> <Button size="2xl" color="indigo" @@ -71,7 +76,6 @@ export function ShareModal(props: { {linkIcon} Copy link </Button> - <Row className="z-0 flex-wrap justify-center gap-4 self-center"> <TweetButton className="self-start" From b9fffcfa305c6b10c1fb0c006c429e83c70ff24e Mon Sep 17 00:00:00 2001 From: mantikoros <sgrugett@gmail.com> Date: Thu, 22 Sep 2022 14:20:37 -0400 Subject: [PATCH 16/23] sort: add back 24h volume, remove most traded --- web/components/contract-search.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/components/contract-search.tsx b/web/components/contract-search.tsx index 4b14d4a7..919cce86 100644 --- a/web/components/contract-search.tsx +++ b/web/components/contract-search.tsx @@ -48,7 +48,7 @@ import { Title } from './title' export const SORTS = [ { label: 'Newest', value: 'newest' }, { label: 'Trending', value: 'score' }, - { label: `Most traded`, value: 'most-traded' }, + { label: '24h volume', value: '24-hour-vol' }, { label: 'Last updated', value: 'last-updated' }, { label: 'Closing soon', value: 'close-date' }, { label: 'Resolve date', value: 'resolve-date' }, From 6fe0a22a4867f789e8e7d2109a5fb8054625f1a6 Mon Sep 17 00:00:00 2001 From: Marshall Polaris <marshall@pol.rs> Date: Thu, 22 Sep 2022 12:40:27 -0700 Subject: [PATCH 17/23] Improve contract leaderboard computation (#918) * Fix and clean up top comment stuff * Make leaderboard code generic on entry type * No need to look up users on contract leaderboard --- .../contract/contract-leaderboard.tsx | 70 +++++++------------ web/components/leaderboard.tsx | 31 ++++---- web/pages/group/[...slugs]/index.tsx | 2 +- web/pages/leaderboards.tsx | 6 +- 4 files changed, 48 insertions(+), 61 deletions(-) diff --git a/web/components/contract/contract-leaderboard.tsx b/web/components/contract/contract-leaderboard.tsx index 6cd2ae62..f984e3b6 100644 --- a/web/components/contract/contract-leaderboard.tsx +++ b/web/components/contract/contract-leaderboard.tsx @@ -3,9 +3,8 @@ 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 { memo } from 'react' import { useComments } from 'web/hooks/use-comments' -import { listUsers, User } from 'web/lib/firebase/users' import { FeedBet } from '../feed/feed-bets' import { FeedComment } from '../feed/feed-comments' import { Spacer } from '../layout/spacer' @@ -13,59 +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[] }) { const { contract, bets } = props // todo: this stuff should be calced in DB at resolve time const comments = useComments(contract.id) - const commentsById = keyBy(comments, 'id') const betsById = keyBy(bets, 'id') // If 'id2' is the sale of 'id1', both are logged with (id2 - id1) of profit @@ -86,29 +74,23 @@ export function ContractTopTrades(props: { contract: Contract; bets: Bet[] }) { 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/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/pages/group/[...slugs]/index.tsx b/web/pages/group/[...slugs]/index.tsx index 779677c4..f06247cd 100644 --- a/web/pages/group/[...slugs]/index.tsx +++ b/web/pages/group/[...slugs]/index.tsx @@ -403,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/leaderboards.tsx b/web/pages/leaderboards.tsx index 623d21c8..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', From 6ee8d90bdb8a12657471b41dc48507a96c5001b3 Mon Sep 17 00:00:00 2001 From: Marshall Polaris <marshall@pol.rs> Date: Thu, 22 Sep 2022 12:40:44 -0700 Subject: [PATCH 18/23] Eliminate redundant showReply/replyTo state (#917) --- web/components/comment-input.tsx | 15 ++++--- .../feed/feed-answer-comment-group.tsx | 39 +++++++------------ web/components/feed/feed-comments.tsx | 30 +++++++------- web/posts/post-comments.tsx | 2 +- 4 files changed, 35 insertions(+), 51 deletions(-) diff --git a/web/components/comment-input.tsx b/web/components/comment-input.tsx index bf3730f3..d13ebf3b 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: { <CommentInputTextArea editor={editor} upload={upload} - replyToUser={replyToUser} + replyTo={replyTo} user={user} submitComment={submitComment} isSubmitting={isSubmitting} @@ -67,14 +67,13 @@ export function CommentInput(props: { export function CommentInputTextArea(props: { user: User | undefined | null - replyToUser?: { id: string; username: string } + replyTo?: { id: string; username: string } editor: Editor | null upload: Parameters<typeof TextEditor>[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() diff --git a/web/components/feed/feed-answer-comment-group.tsx b/web/components/feed/feed-answer-comment-group.tsx index 27f0f731..b4f822cb 100644 --- a/web/components/feed/feed-answer-comment-group.tsx +++ b/web/components/feed/feed-answer-comment-group.tsx @@ -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,27 +26,11 @@ 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 [replyTo, setReplyTo] = useState<ReplyTo>() const [highlighted, setHighlighted] = useState(false) 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) - } - ) - useEffect(() => { if (router.asPath.endsWith(`#${answerElementId}`)) { setHighlighted(true) @@ -83,7 +66,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 +77,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 +94,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 +109,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..94cea452 100644 --- a/web/components/feed/feed-comments.tsx +++ b/web/components/feed/feed-comments.tsx @@ -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 { @@ -174,7 +170,7 @@ export function FeedComment(props: { {onReplyClick && ( <button className="font-bold hover:underline" - onClick={() => onReplyClick(comment)} + onClick={onReplyClick} > Reply </button> @@ -204,7 +200,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 +222,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/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} /> From 721448f40811889cb8780f7c1ea5c537fbbc4fb1 Mon Sep 17 00:00:00 2001 From: Marshall Polaris <marshall@pol.rs> Date: Thu, 22 Sep 2022 12:40:55 -0700 Subject: [PATCH 19/23] Clean up and fix stuff on answers panel (#914) --- web/components/answers/answers-panel.tsx | 41 ++++++++---------------- 1 file changed, 14 insertions(+), 27 deletions(-) diff --git a/web/components/answers/answers-panel.tsx b/web/components/answers/answers-panel.tsx index a1cef4c3..51cf5799 100644 --- a/web/components/answers/answers-panel.tsx +++ b/web/components/answers/answers-panel.tsx @@ -38,26 +38,13 @@ 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 sortedAnswers = [ - ...sortBy(winningAnswers, (answer) => - resolutions ? -1 * resolutions[answer.id] : 0 - ), - ...sortBy( - resolution ? [] : losingAnswers, - (answer) => -1 * getDpmOutcomeProbability(contract.totalShares, answer.id) - ), - ] - - const answerItems = sortBy( - losingAnswers.length > 0 ? losingAnswers : sortedAnswers, - (answer) => -getOutcomeProbability(contract, answer.id) + const [visibleAnswers, invisibleAnswers] = partition( + sortBy(notWinningAnswers, (a) => -getOutcomeProbability(contract, a.id)), + (a) => showAllAnswers || totalBets[a.id] > 0 ) const user = useUser() @@ -107,13 +94,13 @@ export function AnswersPanel(props: { return ( <Col className="gap-3"> {(resolveOption || resolution) && - sortedAnswers.map((answer) => ( + sortBy(winningAnswers, (a) => -(resolutions?.[a.id] ?? 0)).map((a) => ( <AnswerItem - key={answer.id} - answer={answer} + key={a.id} + answer={a} contract={contract} showChoice={showChoice} - chosenProb={chosenAnswers[answer.id]} + chosenProb={chosenAnswers[a.id]} totalChosenProb={chosenTotal} onChoose={onChoose} onDeselect={onDeselect} @@ -127,10 +114,10 @@ export function AnswersPanel(props: { tradingAllowed(contract) ? '' : '-mb-6' )} > - {answerItems.map((item) => ( - <OpenAnswer key={item.id} answer={item} contract={contract} /> + {visibleAnswers.map((a) => ( + <OpenAnswer key={a.id} answer={a} contract={contract} /> ))} - {hasZeroBetAnswers && !showAllAnswers && ( + {invisibleAnswers.length > 0 && !showAllAnswers && ( <Button className="self-end" color="gray-white" @@ -143,7 +130,7 @@ export function AnswersPanel(props: { </Col> )} - {answers.length <= 1 && ( + {answers.length === 0 && ( <div className="pb-4 text-gray-500">No answers yet...</div> )} From 7704de6904743a40f20f47d0e505b939381bdb14 Mon Sep 17 00:00:00 2001 From: Marshall Polaris <marshall@pol.rs> Date: Thu, 22 Sep 2022 12:46:48 -0700 Subject: [PATCH 20/23] Next.js 12.2.5 -> 12.3.1 (#922) --- web/components/auth-context.tsx | 2 +- web/components/avatar.tsx | 2 +- web/components/landing-page-panel.tsx | 1 + web/next.config.js | 3 - web/package.json | 2 +- web/pages/_app.tsx | 9 +- yarn.lock | 175 +++++++++++++------------- 7 files changed, 100 insertions(+), 94 deletions(-) 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/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/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/_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/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" From a1c3d0a2dde77024c2705c4bb98243d00d582580 Mon Sep 17 00:00:00 2001 From: Marshall Polaris <marshall@pol.rs> Date: Thu, 22 Sep 2022 12:58:40 -0700 Subject: [PATCH 21/23] Fix up comment permalink stuff (#915) * Eliminate needless state/effects to highlight comments * Scroll to comment on render if highlighted --- web/components/feed/feed-answer-comment-group.tsx | 12 +++++++----- web/components/feed/feed-comments.tsx | 13 ++++++++----- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/web/components/feed/feed-answer-comment-group.tsx b/web/components/feed/feed-answer-comment-group.tsx index b4f822cb..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' @@ -27,15 +27,16 @@ export function FeedAnswerCommentGroup(props: { const { username, avatarUrl, name, text } = answer const [replyTo, setReplyTo] = useState<ReplyTo>() - const [highlighted, setHighlighted] = useState(false) const router = useRouter() const answerElementId = `answer-${answer.id}` + 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"> @@ -44,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} /> diff --git a/web/components/feed/feed-comments.tsx b/web/components/feed/feed-comments.tsx index 94cea452..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' @@ -94,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', From 2240db9baaaab63fa3235eb9f756535b4a228c74 Mon Sep 17 00:00:00 2001 From: Sinclair Chen <abc.sinclair@gmail.com> Date: Thu, 22 Sep 2022 16:24:30 -0400 Subject: [PATCH 22/23] fix profile tab styling --- web/components/layout/tabs.tsx | 2 +- web/components/user-page.tsx | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/web/components/layout/tabs.tsx b/web/components/layout/tabs.tsx index 45e7e297..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(' 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/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} /> From eaaa46294af242e5d0c7ffaa5b3abc08be0b8032 Mon Sep 17 00:00:00 2001 From: Sinclair Chen <abc.sinclair@gmail.com> Date: Thu, 22 Sep 2022 17:07:51 -0400 Subject: [PATCH 23/23] fix empty comment send button style --- web/components/comment-input.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/components/comment-input.tsx b/web/components/comment-input.tsx index d13ebf3b..3ba6f2ce 100644 --- a/web/components/comment-input.tsx +++ b/web/components/comment-input.tsx @@ -126,7 +126,7 @@ export function CommentInputTextArea(props: { <TextEditor editor={editor} upload={upload}> {user && !isSubmitting && ( <button - className="btn btn-ghost btn-sm px-2 disabled:bg-inherit disabled:text-gray-300" + className="btn btn-ghost btn-sm disabled:bg-inherit! px-2 disabled:text-gray-300" disabled={!editor || editor.isEmpty} onClick={submit} >