- {answerItems.map((item) => (
+ {answerItems.map((item, activityItemIdx) => (
diff --git a/web/components/bets-list.tsx b/web/components/bets-list.tsx
index d227ac88..a4e90c35 100644
--- a/web/components/bets-list.tsx
+++ b/web/components/bets-list.tsx
@@ -138,8 +138,9 @@ export function BetsList(props: { user: User; hideBetsBefore?: number }) {
return !hasSoldAll
})
- const unsettled = contracts.filter(
- (c) => !c.isResolved && contractsMetrics[c.id].invested !== 0
+ const [settled, unsettled] = partition(
+ contracts,
+ (c) => c.isResolved || contractsMetrics[c.id].invested === 0
)
const currentInvested = sumBy(
@@ -260,7 +261,7 @@ function ContractBets(props: {
const isBinary = outcomeType === 'BINARY'
- const { payout, profit, profitPercent } = getContractBetMetrics(
+ const { payout, profit, profitPercent, invested } = getContractBetMetrics(
contract,
bets
)
@@ -656,6 +657,7 @@ function SellButton(props: { contract: Contract; bet: Bet }) {
return (
txn.amount)
diff --git a/web/components/choices-toggle-group.tsx b/web/components/choices-toggle-group.tsx
index 61c4e4fd..f974d72f 100644
--- a/web/components/choices-toggle-group.tsx
+++ b/web/components/choices-toggle-group.tsx
@@ -24,12 +24,13 @@ export function ChoicesToggleGroup(props: {
null}
>
{Object.keys(choicesMap).map((choiceKey) => (
setChoice(choicesMap[choiceKey])}
className={({ active }) =>
clsx(
active ? 'ring-2 ring-indigo-500 ring-offset-2' : '',
diff --git a/web/components/confirmation-button.tsx b/web/components/confirmation-button.tsx
index 57a7bafe..e895467a 100644
--- a/web/components/confirmation-button.tsx
+++ b/web/components/confirmation-button.tsx
@@ -5,6 +5,7 @@ import { Modal } from './layout/modal'
import { Row } from './layout/row'
export function ConfirmationButton(props: {
+ id: string
openModalBtn: {
label: string
icon?: JSX.Element
@@ -21,7 +22,7 @@ export function ConfirmationButton(props: {
onSubmit: () => void
children: ReactNode
}) {
- const { openModalBtn, cancelBtn, submitBtn, onSubmit, children } = props
+ const { id, openModalBtn, cancelBtn, submitBtn, onSubmit, children } = props
const [open, setOpen] = useState(false)
@@ -66,6 +67,7 @@ export function ResolveConfirmationButton(props: {
props
return (
@@ -257,7 +257,7 @@ function QuickOutcomeView(props: {
// If there's a preview prob, display that instead of the current prob
const override =
previewProb === undefined ? undefined : formatPercent(previewProb)
- const textColor = `text-${getColor(contract)}`
+ const textColor = `text-${getColor(contract, previewProb)}`
let display: string | undefined
switch (outcomeType) {
@@ -306,7 +306,7 @@ function getNumericScale(contract: NumericContract) {
return (ev - min) / (max - min)
}
-export function getColor(contract: Contract) {
+export function getColor(contract: Contract, previewProb?: number) {
// TODO: Try injecting a gradient here
// return 'primary'
const { resolution } = contract
diff --git a/web/components/feed-create.tsx b/web/components/feed-create.tsx
index 0a9b77e3..98b56d69 100644
--- a/web/components/feed-create.tsx
+++ b/web/components/feed-create.tsx
@@ -1,10 +1,16 @@
-import { SparklesIcon } from '@heroicons/react/solid'
+import { sample } from 'lodash'
+import { SparklesIcon, XIcon } from '@heroicons/react/solid'
+import { Avatar } from './avatar'
+import { useEffect, useRef, useState } from 'react'
import { Spacer } from './layout/spacer'
-import { firebaseLogin } from 'web/lib/firebase/users'
+import { NewContract } from '../pages/create'
+import { firebaseLogin, User } from 'web/lib/firebase/users'
import { ContractsGrid } from './contract/contracts-list'
-import { Contract } from 'common/contract'
+import { Contract, MAX_QUESTION_LENGTH } from 'common/contract'
import { Col } from './layout/col'
+import clsx from 'clsx'
import { Row } from './layout/row'
+import { ENV_CONFIG } from '../../common/envs/constants'
import { SiteLink } from './site-link'
import { formatMoney } from 'common/util/format'
@@ -61,3 +67,90 @@ export function FeedPromo(props: { hotContracts: Contract[] }) {
>
)
}
+
+export default function FeedCreate(props: {
+ user?: User
+ tag?: string
+ placeholder?: string
+ className?: string
+}) {
+ const { user, tag, className } = props
+ const [question, setQuestion] = useState('')
+ const [isExpanded, setIsExpanded] = useState(false)
+ const inputRef = useRef()
+
+ // Rotate through a new placeholder each day
+ // Easter egg idea: click your own name to shuffle the placeholder
+ // const daysSinceEpoch = Math.floor(Date.now() / 1000 / 60 / 60 / 24)
+
+ // Take care not to produce a different placeholder on the server and client
+ const [defaultPlaceholder, setDefaultPlaceholder] = useState('')
+ useEffect(() => {
+ setDefaultPlaceholder(`e.g. ${sample(ENV_CONFIG.newQuestionPlaceholders)}`)
+ }, [])
+
+ const placeholder = props.placeholder ?? defaultPlaceholder
+
+ return (
+ {
+ !isExpanded && inputRef.current?.focus()
+ }}
+ >
+
+
+
+
+
+ Ask a question...
+ {isExpanded && (
+
+ )}
+
+
+
+
+ {/* Hide component instead of deleting, so edits to NewContract don't get lost */}
+
+
+
+
+ {/* Show a fake "Create Market" button, which gets replaced with the NewContract one*/}
+ {!isExpanded && (
+
+
+
+ )}
+
+ )
+}
diff --git a/web/components/feed/feed-answer-comment-group.tsx b/web/components/feed/feed-answer-comment-group.tsx
index 7e9e19aa..e42375d4 100644
--- a/web/components/feed/feed-answer-comment-group.tsx
+++ b/web/components/feed/feed-answer-comment-group.tsx
@@ -2,7 +2,7 @@ import { Answer } from 'common/answer'
import { Bet } from 'common/bet'
import { Comment } from 'common/comment'
import { formatPercent } from 'common/util/format'
-import React, { useEffect, useState } from 'react'
+import React, { useEffect, useMemo, useState } from 'react'
import { Col } from 'web/components/layout/col'
import { Modal } from 'web/components/layout/modal'
import { AnswerBetPanel } from 'web/components/answers/answer-bet-panel'
diff --git a/web/components/feed/feed-items.tsx b/web/components/feed/feed-items.tsx
index 2a53b3a7..05ea14ff 100644
--- a/web/components/feed/feed-items.tsx
+++ b/web/components/feed/feed-items.tsx
@@ -3,6 +3,7 @@ import React, { useState } from 'react'
import {
BanIcon,
CheckIcon,
+ DotsVerticalIcon,
LockClosedIcon,
XIcon,
} from '@heroicons/react/solid'
@@ -273,3 +274,30 @@ function FeedClose(props: { contract: Contract }) {
>
)
}
+
+// TODO: Should highlight the entire Feed segment
+function FeedExpand(props: { setExpanded: (expanded: boolean) => void }) {
+ const { setExpanded } = props
+ return (
+ <>
+
+
+
+ >
+ )
+}
diff --git a/web/components/folds/create-fold-button.tsx b/web/components/folds/create-fold-button.tsx
index 770ad65f..042982ea 100644
--- a/web/components/folds/create-fold-button.tsx
+++ b/web/components/folds/create-fold-button.tsx
@@ -48,6 +48,7 @@ export function CreateFoldButton() {
return (
,
diff --git a/web/components/liquidity-panel.tsx b/web/components/liquidity-panel.tsx
index 63515c23..9e09c93d 100644
--- a/web/components/liquidity-panel.tsx
+++ b/web/components/liquidity-panel.tsx
@@ -98,7 +98,7 @@ function AddLiquidityPanel(props: { contract: CPMMContract }) {
setError('Server error')
}
})
- .catch((_) => setError('Server error'))
+ .catch((e) => setError('Server error'))
}
return (
@@ -162,7 +162,7 @@ function WithdrawLiquidityPanel(props: {
const { contract, lpShares } = props
const { YES: yesShares, NO: noShares } = lpShares
- const [_error, setError] = useState(undefined)
+ const [error, setError] = useState(undefined)
const [isSuccess, setIsSuccess] = useState(false)
const [isLoading, setIsLoading] = useState(false)
@@ -171,12 +171,12 @@ function WithdrawLiquidityPanel(props: {
setIsSuccess(false)
withdrawLiquidity({ contractId: contract.id })
- .then((_) => {
+ .then((r) => {
setIsSuccess(true)
setError(undefined)
setIsLoading(false)
})
- .catch((_) => setError('Server error'))
+ .catch((e) => setError('Server error'))
}
if (isSuccess)
diff --git a/web/components/nav/profile-menu.tsx b/web/components/nav/profile-menu.tsx
index 9af9f59c..e026b35d 100644
--- a/web/components/nav/profile-menu.tsx
+++ b/web/components/nav/profile-menu.tsx
@@ -1,7 +1,8 @@
import Link from 'next/link'
-import { User } from 'web/lib/firebase/users'
+import { firebaseLogout, User } from 'web/lib/firebase/users'
import { formatMoney } from 'common/util/format'
import { Avatar } from '../avatar'
+import { IS_PRIVATE_MANIFOLD } from 'common/envs/constants'
export function ProfileSummary(props: { user: User }) {
const { user } = props
diff --git a/web/components/nav/sidebar.tsx b/web/components/nav/sidebar.tsx
index acb358ba..457b69ff 100644
--- a/web/components/nav/sidebar.tsx
+++ b/web/components/nav/sidebar.tsx
@@ -7,12 +7,15 @@ import {
CashIcon,
HeartIcon,
PresentationChartLineIcon,
+ ChatAltIcon,
SparklesIcon,
NewspaperIcon,
} from '@heroicons/react/outline'
import clsx from 'clsx'
+import { sortBy } from 'lodash'
import Link from 'next/link'
import { useRouter } from 'next/router'
+import { useFollowedFolds } from 'web/hooks/use-fold'
import { useUser } from 'web/hooks/use-user'
import { firebaseLogin, firebaseLogout, User } from 'web/lib/firebase/users'
import { ManifoldLogo } from './manifold-logo'
@@ -176,6 +179,8 @@ export default function Sidebar(props: { className?: string }) {
}, [])
const user = useUser()
+ let folds = useFollowedFolds(user) || []
+ folds = sortBy(folds, 'followCount').reverse()
const mustWaitForFreeMarketStatus = useHasCreatedContractToday(user)
const navigationOptions =
user === null
diff --git a/web/components/numeric-bet-panel.tsx b/web/components/numeric-bet-panel.tsx
index df52c0f7..ebb80dd0 100644
--- a/web/components/numeric-bet-panel.tsx
+++ b/web/components/numeric-bet-panel.tsx
@@ -53,6 +53,7 @@ function NumericBuyPanel(props: {
const [betAmount, setBetAmount] = useState(undefined)
+ const [valueError, setValueError] = useState()
const [error, setError] = useState()
const [isSubmitting, setIsSubmitting] = useState(false)
const [wasSubmitted, setWasSubmitted] = useState(false)
diff --git a/web/components/page.tsx b/web/components/page.tsx
index c33083c7..421722a3 100644
--- a/web/components/page.tsx
+++ b/web/components/page.tsx
@@ -6,11 +6,12 @@ import { Toaster } from 'react-hot-toast'
export function Page(props: {
margin?: boolean
+ assertUser?: 'signed-in' | 'signed-out'
rightSidebar?: ReactNode
suspend?: boolean
children?: ReactNode
}) {
- const { margin, children, rightSidebar, suspend } = props
+ const { margin, assertUser, children, rightSidebar, suspend } = props
return (
<>
diff --git a/web/hooks/use-fold.ts b/web/hooks/use-fold.ts
index e00ea12a..f6335ebf 100644
--- a/web/hooks/use-fold.ts
+++ b/web/hooks/use-fold.ts
@@ -1,7 +1,9 @@
+import { isEqual, sortBy } from 'lodash'
import { useEffect, useState } from 'react'
import { Fold } from 'common/fold'
import { User } from 'common/user'
import {
+ listAllFolds,
listenForFold,
listenForFolds,
listenForFoldsWithTags,
@@ -78,3 +80,38 @@ export const useFollowedFoldIds = (user: User | null | undefined) => {
return followedFoldIds
}
+
+// We also cache followedFolds directly in JSON.
+// TODO: Extract out localStorage caches to a utility
+export const useFollowedFolds = (user: User | null | undefined) => {
+ const [followedFolds, setFollowedFolds] = useState()
+ const ids = useFollowedFoldIds(user)
+
+ useEffect(() => {
+ if (user && ids) {
+ const key = `followed-full-folds-${user.id}`
+ const followedFoldJson = localStorage.getItem(key)
+ if (followedFoldJson) {
+ setFollowedFolds(JSON.parse(followedFoldJson))
+ // Exit early if ids and followedFoldIds have all the same elements.
+ if (
+ isEqual(
+ sortBy(ids),
+ sortBy(JSON.parse(followedFoldJson).map((f: Fold) => f.id))
+ )
+ ) {
+ return
+ }
+ }
+
+ // Otherwise, fetch the full contents of all folds
+ listAllFolds().then((folds) => {
+ const followedFolds = folds.filter((fold) => ids.includes(fold.id))
+ setFollowedFolds(followedFolds)
+ localStorage.setItem(key, JSON.stringify(followedFolds))
+ })
+ }
+ }, [user, ids])
+
+ return followedFolds
+}
diff --git a/web/lib/firebase/notifications.ts b/web/lib/firebase/notifications.ts
index c0dca8be..c4a30300 100644
--- a/web/lib/firebase/notifications.ts
+++ b/web/lib/firebase/notifications.ts
@@ -1,7 +1,7 @@
import { collection, query, where } from 'firebase/firestore'
import { Notification } from 'common/notification'
import { db } from 'web/lib/firebase/init'
-import { listenForValues } from 'web/lib/firebase/utils'
+import { getValues, listenForValues } from 'web/lib/firebase/utils'
function getNotificationsQuery(userId: string, unseenOnly?: boolean) {
const notifsCollection = collection(db, `/users/${userId}/notifications`)
diff --git a/web/pages/activity.tsx b/web/pages/activity.tsx
index 5d1a0a18..b39191bc 100644
--- a/web/pages/activity.tsx
+++ b/web/pages/activity.tsx
@@ -37,7 +37,7 @@ export default function Activity() {
return (
<>
-
+
diff --git a/web/pages/api/v0/market/[id].ts b/web/pages/api/v0/market/[id].ts
index 8f33276d..723a6440 100644
--- a/web/pages/api/v0/market/[id].ts
+++ b/web/pages/api/v0/market/[id].ts
@@ -19,7 +19,6 @@ export default async function handler(
listAllComments(contractId),
])
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
const bets = allBets.map(({ userId, ...bet }) => bet) as Exclude<
Bet,
'userId'
diff --git a/web/pages/api/v0/slug/[slug].ts b/web/pages/api/v0/slug/[slug].ts
index 119a688b..fba785fb 100644
--- a/web/pages/api/v0/slug/[slug].ts
+++ b/web/pages/api/v0/slug/[slug].ts
@@ -24,7 +24,6 @@ export default async function handler(
listAllComments(contract.id),
])
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
const bets = allBets.map(({ userId, ...bet }) => bet) as Exclude<
Bet,
'userId'
diff --git a/web/pages/create.tsx b/web/pages/create.tsx
index 1e66a01c..259a5375 100644
--- a/web/pages/create.tsx
+++ b/web/pages/create.tsx
@@ -56,8 +56,8 @@ export default function Create() {
}
// Allow user to create a new contract
-export function NewContract(props: { question: string }) {
- const { question } = props
+export function NewContract(props: { question: string; tag?: string }) {
+ const { question, tag } = props
const creator = useUser()
useEffect(() => {
@@ -74,7 +74,7 @@ export function NewContract(props: { question: string }) {
// const [tagText, setTagText] = useState(tag ?? '')
// const tags = parseWordsAsTags(tagText)
- const [ante, _setAnte] = useState(FIXED_ANTE)
+ const [ante, setAnte] = useState(FIXED_ANTE)
const mustWaitForDailyFreeMarketStatus = useHasCreatedContractToday(creator)
diff --git a/web/pages/fold/[...slugs]/index.tsx b/web/pages/fold/[...slugs]/index.tsx
index b0ab8e40..095cf274 100644
--- a/web/pages/fold/[...slugs]/index.tsx
+++ b/web/pages/fold/[...slugs]/index.tsx
@@ -27,8 +27,10 @@ import { EditFoldButton } from 'web/components/folds/edit-fold-button'
import Custom404 from '../../404'
import { FollowFoldButton } from 'web/components/folds/follow-fold-button'
import { SEO } from 'web/components/SEO'
+import { useTaggedContracts } from 'web/hooks/use-contracts'
import { Linkify } from 'web/components/linkify'
import { fromPropz, usePropz } from 'web/hooks/use-propz'
+import { filterDefined } from 'common/util/array'
import { findActiveContracts } from 'web/components/feed/find-active-contracts'
import { Tabs } from 'web/components/layout/tabs'
@@ -131,6 +133,15 @@ export default function FoldPage(props: {
const user = useUser()
const isCurator = user && fold && user.id === fold.curatorId
+ const taggedContracts = useTaggedContracts(fold?.tags) ?? props.contracts
+ const contractsMap = Object.fromEntries(
+ taggedContracts.map((contract) => [contract.id, contract])
+ )
+
+ const contracts = filterDefined(
+ props.contracts.map((contract) => contractsMap[contract.id])
+ )
+
if (fold === null || !foldSubpages.includes(page) || slugs[2]) {
return
}
diff --git a/web/pages/home.tsx b/web/pages/home.tsx
index ccf9a585..39671019 100644
--- a/web/pages/home.tsx
+++ b/web/pages/home.tsx
@@ -23,7 +23,7 @@ const Home = () => {
return (
<>
-
+
{
}
return (
-
+
diff --git a/web/pages/landing-page.tsx b/web/pages/landing-page.tsx
index 860ccfb2..dd9c0b09 100644
--- a/web/pages/landing-page.tsx
+++ b/web/pages/landing-page.tsx
@@ -8,14 +8,19 @@ import {
} from '@heroicons/react/outline'
import { firebaseLogin } from 'web/lib/firebase/users'
+import { ContractsGrid } from 'web/components/contract/contracts-list'
import { Col } from 'web/components/layout/col'
import Link from 'next/link'
+import { Contract } from 'web/lib/firebase/contracts'
+
+export default function LandingPage(props: { hotContracts: Contract[] }) {
+ const { hotContracts } = props
-export default function LandingPage() {
return (
+ {/* */}
)
}
@@ -144,3 +149,20 @@ function FeaturesSection() {
)
}
+
+function ExploreMarketsSection(props: { hotContracts: Contract[] }) {
+ const { hotContracts } = props
+ return (
+
+
+ Today's top markets
+
+
+
{}}
+ hasMore={false}
+ />
+
+ )
+}
diff --git a/web/pages/leaderboards.tsx b/web/pages/leaderboards.tsx
index 1d76ec45..f2159b80 100644
--- a/web/pages/leaderboards.tsx
+++ b/web/pages/leaderboards.tsx
@@ -8,8 +8,8 @@ import { fromPropz, usePropz } from 'web/hooks/use-propz'
export const getStaticProps = fromPropz(getStaticPropz)
export async function getStaticPropz() {
const [topTraders, topCreators] = await Promise.all([
- getTopTraders().catch(() => {}),
- getTopCreators().catch(() => {}),
+ getTopTraders().catch((_) => {}),
+ getTopCreators().catch((_) => {}),
])
return {
diff --git a/web/pages/make-predictions.tsx b/web/pages/make-predictions.tsx
index ce694278..8b35dd16 100644
--- a/web/pages/make-predictions.tsx
+++ b/web/pages/make-predictions.tsx
@@ -290,3 +290,13 @@ ${TEST_VALUE}
)
}
+
+// Given a date string like '2022-04-02',
+// return the time just before midnight on that date (in the user's local time), as millis since epoch
+function dateToMillis(date: string) {
+ return dayjs(date)
+ .set('hour', 23)
+ .set('minute', 59)
+ .set('second', 59)
+ .valueOf()
+}
diff --git a/web/pages/notifications.tsx b/web/pages/notifications.tsx
index 02fd9d73..8130ed22 100644
--- a/web/pages/notifications.tsx
+++ b/web/pages/notifications.tsx
@@ -31,7 +31,9 @@ import { Linkify } from 'web/components/linkify'
import {
BinaryOutcomeLabel,
CancelLabel,
+ FreeResponseOutcomeLabel,
MultiLabel,
+ OutcomeLabel,
ProbPercentLabel,
} from 'web/components/outcome-label'
import {
@@ -42,7 +44,7 @@ import {
import { getContractFromId } from 'web/lib/firebase/contracts'
import { CheckIcon, XIcon } from '@heroicons/react/outline'
import toast from 'react-hot-toast'
-import { formatMoney } from 'common/util/format'
+import { formatMoney, formatPercent } from 'common/util/format'
export default function Notifications() {
const user = useUser()
@@ -182,7 +184,7 @@ function NotificationGroupItem(props: {
className?: string
}) {
const { notificationGroup, className } = props
- const { sourceContractId, notifications } = notificationGroup
+ const { sourceContractId, notifications, timePeriod } = notificationGroup
const {
sourceContractTitle,
sourceContractSlug,
diff --git a/web/pages/stats.tsx b/web/pages/stats.tsx
index c73b6821..a4e2790d 100644
--- a/web/pages/stats.tsx
+++ b/web/pages/stats.tsx
@@ -1,5 +1,6 @@
import dayjs from 'dayjs'
import { zip, uniq, sumBy, concat, countBy, sortBy, sum } from 'lodash'
+import { IS_PRIVATE_MANIFOLD } from 'common/envs/constants'
import {
DailyCountChart,
DailyPercentChart,