Merge branch 'main' into CPM-ui

This commit is contained in:
Austin Chen 2022-04-28 19:01:08 -04:00
commit d55cf571c5
22 changed files with 452 additions and 183 deletions

View File

@ -1,14 +1,51 @@
# Manifold Markets
This [monorepo][] has basically everything involved in running and operating Manifold.
## Getting started
0. Make sure you have [Yarn 1.x][yarn]
1. `$ cd web`
2. `$ yarn`
3. `$ yarn dev:dev`
4. Your site will be available on http://localhost:3000
See [`web/README.md`][web-readme] for more details on hacking on the web client.
## General architecture
Manifold's public API and web app are hosted by [Vercel][vercel]. In general, the web app runs as much as possible on the client; we follow a [JAMStack][jamstack] architecture. All data is stored in Firebase's database, [Cloud Firestore][cloud-firestore]. This is directly accessed by the client for most data access operations.
Operations with complicated contracts (e.g. buying shares) are provided in a separate HTTP API using Firebase's [Cloud Functions][cloud-functions]. Those are deployed separately from the Vercel website; see [`functions/README.md`][functions-readme] for more details.
## Directory overview
- `web/`: UI and biz logic. Where most of the site lives
- `functions/`: Firebase cloud functions, for secure work (balances, Stripe payments, emails)
- `common/`: shared between web & functions
- `og-image/`: The OpenGraph image generator; creates the preview images shown on Twitter/social media
- `web/`: UI and business logic for the client. Where most of the site lives. The public API endpoints are also in here.
## Philosophies
- `functions/`: Firebase cloud functions, for secure work (e.g. balances, Stripe payments, emails). Also contains in
`functions/src/scripts/` some Typescript scripts that do ad hoc CLI interaction with Firebase.
- [JAMStack](https://jamstack.org/): Keep things simple, no servers
- [Monorepo](https://semaphoreci.com/blog/what-is-monorepo): Good fit for our current size
- [Small PRs](https://google.github.io/eng-practices/review/developer/small-cls.html): Lots of little changes > one big diff
- `common/`: Typescript library code shared between `web/` & `functions/`. If you want to look at how the market math
works, most of that's in here (it gets called from the `placeBet` and `sellBet` endpoints in `functions/`.) Also
contains in `common/envs` configuration for the different environments (i.e. prod, dev, Manifold for Teams instances.)
- `og-image/`: The OpenGraph image generator; creates the preview images shown on Twitter/social media.
Also: Our docs are currently in [a separate repo](https://github.com/manifoldmarkets/docs). TODO: move them into this monorepo.
## Contributing
Since we are just now open-sourcing things, we will see how things go. Feel free to open issues, submit PRs, and chat about the process on [Discord][discord]. We would prefer [small PRs][small-prs] that we can effectively evaluate and review -- maybe check in with us first if you are thinking to work on a big change.
If you need additional access to any infrastructure in order to work on something (e.g. Vercel, Firebase) let us know about that on Discord as well.
[vercel]: https://vercel.com/
[jamstack]: https://jamstack.org/
[monorepo]: https://semaphoreci.com/blog/what-is-monorepo
[yarn]: https://classic.yarnpkg.com/lang/en/docs/install/
[web-readme]: https://github.com/manifoldmarkets/manifold/blob/main/web/README.md
[functions-readme]: https://github.com/manifoldmarkets/manifold/blob/main/functions/README.md
[cloud-firestore]: https://firebase.google.com/docs/firestore
[cloud-functions]: https://firebase.google.com/docs/functions
[small-prs]: https://google.github.io/eng-practices/review/developer/small-cls.html
[discord]: https://discord.gg/eHQBNBqXuh

View File

@ -39,16 +39,24 @@ service cloud.firestore {
allow update: if request.resource.data.diff(resource.data).affectedKeys()
.hasOnly(['description', 'closeTime', 'tags', 'lowercaseTags']);
allow update: if isAdmin();
allow delete: if resource.data.creatorId == request.auth.uid;
}
match /{somePath=**}/bets/{betId} {
allow read;
}
function commentMatchesUser(userId, comment) {
// it's a bad look if someone can impersonate other ids/names/avatars so check everything
let user = get(/databases/$(database)/documents/users/$(userId));
return comment.userId == userId
&& comment.userName == user.data.name
&& comment.userUsername == user.data.username
&& comment.userAvatarUrl == user.data.avatarUrl;
}
match /{somePath=**}/comments/{commentId} {
allow read;
allow create: if request.auth != null;
allow create: if request.auth != null && commentMatchesUser(request.auth.uid, request.resource.data);
}
match /{somePath=**}/answers/{answerId} {
@ -57,12 +65,16 @@ service cloud.firestore {
match /folds/{foldId} {
allow read;
allow update, delete: if request.auth.uid == resource.data.curatorId;
allow update: if request.auth.uid == resource.data.curatorId
&& request.resource.data.diff(resource.data).affectedKeys()
.hasOnly(['name', 'about', 'tags', 'lowercaseTags']);
allow delete: if request.auth.uid == resource.data.curatorId;
}
match /{somePath=**}/followers/{userId} {
allow read;
allow write: if request.auth.uid == userId;
allow create, update: if request.auth.uid == userId && request.resource.data.userId == userId;
allow delete: if request.auth.uid == userId;
}
match /txns/{txnId} {

View File

@ -34,8 +34,9 @@ Adapted from https://firebase.google.com/docs/functions/get-started
## Developing locally
0. `$ firebase use dev` if you haven't already
1. `$ yarn serve` to spin up the emulators
The Emulator UI is at http://localhost:4000; the functions are hosted on :5001.
1. The Emulator UI is at http://localhost:4000; the functions are hosted on :5001.
Note: You have to kill and restart emulators when you change code; no hot reload =(
2. `$ yarn dev:emulate` in `/web` to connect to emulators with the frontend
1. Note: emulated database is cleared after every shutdown

View File

@ -48,8 +48,8 @@ export const sendMarketResolutionEmail = async (
creatorName: creator.name,
question: contract.question,
outcome,
investment: `${Math.round(investment)}`,
payout: `${Math.round(payout)}`,
investment: `${Math.floor(investment)}`,
payout: `${Math.floor(payout)}`,
url: `https://${DOMAIN}/${creator.username}/${contract.slug}`,
}
@ -189,7 +189,9 @@ export const sendNewCommentEmail = async (
let betDescription = ''
if (bet) {
const { amount, sale } = bet
betDescription = `${sale ? 'sold' : 'bought'} M$ ${Math.round(amount)}`
betDescription = `${sale || amount < 0 ? 'sold' : 'bought'} ${formatMoney(
Math.abs(amount)
)}`
}
const subject = `Comment on ${question}`

View File

@ -1,24 +1,64 @@
# Manifold Markets web
# Manifold Markets web app
## Getting Started
## Getting started
To run the development server, install [Yarn 1.x][yarn], and then in this directory:
1. `yarn` to install all dependencies
2. `yarn dev:dev` to bring up a local instance, pointing to dev database)
2. `yarn dev:dev` starts a development web server, pointing at the development database
3. Your site will be available on http://localhost:3000
(`yarn dev` will point you to prod database)
Check package.json for other command-line tasks. (e.g. `yarn dev` will point the development server at the prod
database. `yarn emulate` will run against a local emulated database, if you are serving it via `yarn serve` from the
[`functions/` package][functions-readme].)
### Running with local emulated database and functions
## Tech stack
1. `yarn serve` first in `/functions` and wait for it to start
2. `yarn dev:emulate` will point you to the emulated database
Manifold's website uses [Next.js][nextjs], which is a [React][react]-based framework that handles concerns like routing,
builds, and a development server. It's also integrated with [Vercel][vercel], which is responsible for hosting the site
and providing some other production functionality like serving the API. The application code is written exclusively in
Typescript. Styling is done via CSS-in-JS in the React code and uses [Tailwind][tailwind] CSS classes.
## Formatting
## Building and deployment
Before committing, run `yarn format` to format your code.
Vercel's GitHub integration monitors the repository and automatically builds (`next build`) and deploys both the `main`
branch (to production) and PR branches (to ephemeral staging servers that can be used for testing.)
Recommended: Use a [Prettier editor integration](https://prettier.io/docs/en/editors.html) to automatically format on save
Parts of the file structure that directly map to HTTP endpoints are organized specially per Next.js's prescriptions:
### public/
These are static files that will be [served by Next verbatim][next-static-files].
### pages/
These are components that [Next's router][next-pages] is aware of and interprets as page roots per their filename,
e.g. the React component in pages/portfolio.tsx is rendered on the user portfolio page at /portfolio. You should
look in here or in `components/` to find any specific piece of UI you are interested in working on.
### pages/api/
Modules under this route are specially interpreted by Next/Vercel as [functions that will be hosted by
Vercel][vercel-functions]. This is where the public Manifold HTTP API lives.
## Contributing
Please format the code using [Prettier][prettier]; you can run `yarn format` to invoke it manually. It also runs by
default as a pre-commit Git hook thanks to the pretty-quick package. You may wish to use some kind of fancy [editor
integration][prettier-integrations] to format it in your editor.
## Developer Experience TODOs
- Prevent git pushing if there are Typescript errors?
[react]: https://reactjs.org
[nextjs]: https://nextjs.org
[vercel]: https://vercel.com
[tailwind]: https://tailwindcss.com
[yarn]: https://classic.yarnpkg.com/lang/en/docs/install/
[prettier]: https://prettier.io
[prettier-integrations]: https://prettier.io/docs/en/editors.html
[next-static-files]: https://nextjs.org/docs/basic-features/static-file-serving
[next-pages]: https://nextjs.org/docs/basic-features/pages
[vercel-functions]: https://vercel.com/docs/concepts/functions/serverless-functions
[functions-readme]: https://github.com/manifoldmarkets/manifold/blob/main/functions/README.md

View File

@ -11,6 +11,11 @@ import { AnswerItem } from './answer-item'
import { CreateAnswerPanel } from './create-answer-panel'
import { AnswerResolvePanel } from './answer-resolve-panel'
import { Spacer } from '../layout/spacer'
import { FeedItems } from '../feed/feed-items'
import { ActivityItem } from '../feed/activity-items'
import { User } from '../../../common/user'
import { getOutcomeProbability } from '../../../common/calculate'
import { Answer } from '../../../common/answer'
export function AnswersPanel(props: {
contract: FullContract<DPM, FreeResponse>
@ -47,6 +52,8 @@ export function AnswersPanel(props: {
const chosenTotal = _.sum(Object.values(chosenAnswers))
const answerItems = getAnswers(contract, user)
const onChoose = (answerId: string, prob: number) => {
if (resolveOption === 'CHOOSE') {
setChosenAnswers({ [answerId]: prob })
@ -102,6 +109,15 @@ export function AnswersPanel(props: {
<div className="pb-4 text-gray-500">No answers yet...</div>
)}
{!resolveOption && sortedAnswers.length > 0 && (
<FeedItems
contract={contract}
items={answerItems}
className={''}
betRowClassName={''}
/>
)}
{tradingAllowed(contract) &&
(!resolveOption || resolveOption === 'CANCEL') && (
<CreateAnswerPanel contract={contract} />
@ -121,3 +137,32 @@ export function AnswersPanel(props: {
</Col>
)
}
function getAnswers(
contract: FullContract<DPM, FreeResponse>,
user: User | undefined | null
) {
const { answers } = contract
let outcomes = _.uniq(
answers.map((answer) => answer.number.toString())
).filter((outcome) => getOutcomeProbability(contract, outcome) > 0.0001)
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,
items: [] as ActivityItem[],
user,
}
})
.filter((group) => group.answer)
}

View File

@ -21,13 +21,24 @@ export function ContractTabs(props: {
bets.sort((bet1, bet2) => bet2.createdTime - bet1.createdTime)
const userBets = user && bets.filter((bet) => bet.userId === user.id)
const activity = (
const betActivity = (
<ContractActivity
contract={contract}
bets={bets}
comments={comments}
user={user}
mode="all"
mode="bets"
betRowClassName="!mt-0 xl:hidden"
/>
)
const commentActivity = (
<ContractActivity
contract={contract}
bets={bets}
comments={comments}
user={user}
mode="comments"
betRowClassName="!mt-0 xl:hidden"
/>
)
@ -48,10 +59,11 @@ export function ContractTabs(props: {
return (
<Tabs
tabs={[
{ title: 'Timeline', content: activity },
{ title: 'Comments', content: commentActivity },
{ title: 'Bets', content: betActivity },
...(!user || !userBets?.length
? []
: [{ title: 'Your trades', content: yourTrades }]),
: [{ title: 'Your bets', content: yourTrades }]),
]}
/>
)

View File

@ -25,15 +25,16 @@ export function ContractsGrid(props: {
showCloseTime?: boolean
}) {
const { showCloseTime } = props
const PAGE_SIZE = 100
const [page, setPage] = useState(1)
const [resolvedContracts, activeContracts] = _.partition(
props.contracts,
(c) => c.isResolved
)
const contracts = [...activeContracts, ...resolvedContracts].slice(
0,
MAX_CONTRACTS_DISPLAYED
)
const allContracts = [...activeContracts, ...resolvedContracts]
const showMore = allContracts.length > PAGE_SIZE * page
const contracts = allContracts.slice(0, PAGE_SIZE * page)
if (contracts.length === 0) {
return (
@ -47,6 +48,7 @@ export function ContractsGrid(props: {
}
return (
<>
<ul className="grid w-full grid-cols-1 gap-6 md:grid-cols-2">
{contracts.map((contract) => (
<ContractCard
@ -57,6 +59,16 @@ export function ContractsGrid(props: {
/>
))}
</ul>
{/* Show a link that increases the page num when clicked */}
{showMore && (
<button
className="btn btn-link float-right normal-case"
onClick={() => setPage(page + 1)}
>
Show more...
</button>
)}
</>
)
}

View File

@ -31,8 +31,6 @@ type BaseActivityItem = {
export type CommentInputItem = BaseActivityItem & {
type: 'commentInput'
bets: Bet[]
commentsByBetId: Record<string, Comment>
}
export type DescriptionItem = BaseActivityItem & {
@ -68,7 +66,7 @@ export type BetGroupItem = BaseActivityItem & {
}
export type AnswerGroupItem = BaseActivityItem & {
type: 'answergroup'
type: 'answergroup' | 'answer'
answer: Answer
items: ActivityItem[]
}
@ -82,6 +80,7 @@ export type ResolveItem = BaseActivityItem & {
}
const DAY_IN_MS = 24 * 60 * 60 * 1000
const ABBREVIATED_NUM_COMMENTS_OR_BETS_TO_SHOW = 3
// Group together bets that are:
// - Within a day of the first in the group
@ -173,7 +172,9 @@ function groupBets(
if (group.length > 0) {
pushGroup()
}
const abbrItems = abbreviated ? items.slice(-3) : items
const abbrItems = abbreviated
? items.slice(-ABBREVIATED_NUM_COMMENTS_OR_BETS_TO_SHOW)
: items
if (reversed) abbrItems.reverse()
return abbrItems
}
@ -240,7 +241,8 @@ function getAnswerGroups(
reversed,
})
if (abbreviated) items = items.slice(-2)
if (abbreviated)
items = items.slice(-ABBREVIATED_NUM_COMMENTS_OR_BETS_TO_SHOW)
return {
id: outcome,
@ -253,6 +255,8 @@ function getAnswerGroups(
})
.filter((group) => group.answer)
if (reversed) answerGroups.reverse()
return answerGroups
}
@ -268,6 +272,7 @@ function groupBetsAndComments(
reversed: boolean
}
) {
const { smallAvatar, abbreviated, reversed } = options
const commentsWithoutBets = comments
.filter((comment) => !comment.betId)
.map((comment) => ({
@ -276,16 +281,16 @@ function groupBetsAndComments(
contract: contract,
comment,
bet: undefined,
truncate: false,
truncate: abbreviated,
hideOutcome: true,
smallAvatar: false,
smallAvatar,
}))
const groupedBets = groupBets(bets, comments, contract, userId, options)
// iterate through the bets and comment activity items and add them to the items in order of comment creation time:
const unorderedBetsAndComments = [...commentsWithoutBets, ...groupedBets]
const sortedBetsAndComments = _.sortBy(unorderedBetsAndComments, (item) => {
let sortedBetsAndComments = _.sortBy(unorderedBetsAndComments, (item) => {
if (item.type === 'comment') {
return item.comment.createdTime
} else if (item.type === 'bet') {
@ -294,7 +299,13 @@ function groupBetsAndComments(
return item.bets[0].createdTime
}
})
return sortedBetsAndComments
const abbrItems = abbreviated
? sortedBetsAndComments.slice(-ABBREVIATED_NUM_COMMENTS_OR_BETS_TO_SHOW)
: sortedBetsAndComments
if (reversed) abbrItems.reverse()
return abbrItems
}
export function getAllContractActivityItems(
@ -308,8 +319,7 @@ export function getAllContractActivityItems(
) {
const { abbreviated } = options
const { outcomeType } = contract
const reversed = !abbreviated
const reversed = true
bets =
outcomeType === 'BINARY'
@ -328,19 +338,30 @@ export function getAllContractActivityItems(
: [{ type: 'description', id: '0', contract }]
if (outcomeType === 'FREE_RESPONSE') {
const onlyUsersBetsOrBetsWithComments = bets.filter((bet) =>
comments.some(
(comment) => comment.betId === bet.id || bet.userId === user?.id
)
)
items.push(
...getAnswerGroups(
contract as FullContract<DPM, FreeResponse>,
bets,
...groupBetsAndComments(
onlyUsersBetsOrBetsWithComments,
comments,
user,
contract,
user?.id,
{
sortByProb: true,
hideOutcome: false,
abbreviated,
smallAvatar: false,
reversed,
}
)
)
items.push({
type: 'commentInput',
id: 'commentInput',
contract,
})
} else {
items.push(
...groupBetsAndComments(bets, comments, contract, user?.id, {
@ -359,14 +380,13 @@ export function getAllContractActivityItems(
items.push({ type: 'resolve', id: `${contract.resolutionTime}`, contract })
}
const commentsByBetId = mapCommentsByBetId(comments)
if (outcomeType === 'BINARY') {
items.push({
type: 'commentInput',
id: 'commentInput',
bets,
commentsByBetId,
contract,
})
}
if (reversed) items.reverse()
@ -396,9 +416,10 @@ export function getRecentContractActivityItems(
contractPath,
}
const items =
contract.outcomeType === 'FREE_RESPONSE'
? getAnswerGroups(
const items = []
if (contract.outcomeType === 'FREE_RESPONSE') {
items.push(
...getAnswerGroups(
contract as FullContract<DPM, FreeResponse>,
bets,
comments,
@ -406,15 +427,84 @@ export function getRecentContractActivityItems(
{
sortByProb: false,
abbreviated: true,
reversed: false,
reversed: true,
}
)
: groupBetsAndComments(bets, comments, contract, user?.id, {
)
} else {
const onlyUsersBetsOrBetsWithComments = bets.filter((bet) =>
comments.some(
(comment) => comment.betId === bet.id || bet.userId === user?.id
)
)
items.push(
...groupBetsAndComments(
onlyUsersBetsOrBetsWithComments,
comments,
contract,
user?.id,
{
hideOutcome: false,
abbreviated: true,
smallAvatar: false,
reversed: false,
})
reversed: true,
}
)
)
}
return [questionItem, ...items]
}
export function getSpecificContractActivityItems(
contract: Contract,
bets: Bet[],
comments: Comment[],
user: User | null | undefined,
options: {
mode: 'comments' | 'bets'
}
) {
const { mode } = options
let items = [] as ActivityItem[]
switch (mode) {
case 'bets':
items.push(
...groupBets(bets, comments, contract, user?.id, {
hideOutcome: false,
abbreviated: false,
smallAvatar: false,
reversed: false,
})
)
break
case 'comments':
const onlyBetsWithComments = bets.filter((bet) =>
comments.some((comment) => comment.betId === bet.id)
)
items.push(
...groupBetsAndComments(
onlyBetsWithComments,
comments,
contract,
user?.id,
{
hideOutcome: false,
abbreviated: false,
smallAvatar: false,
reversed: false,
}
)
)
items.push({
type: 'commentInput',
id: 'commentInput',
contract,
})
break
}
return items.reverse()
}

View File

@ -1,5 +1,3 @@
import _ from 'lodash'
import { Contract } from '../../lib/firebase/contracts'
import { Comment } from '../../lib/firebase/comments'
import { Bet } from '../../../common/bet'
@ -8,6 +6,7 @@ import { useComments } from '../../hooks/use-comments'
import {
getAllContractActivityItems,
getRecentContractActivityItems,
getSpecificContractActivityItems,
} from './activity-items'
import { FeedItems } from './feed-items'
import { User } from '../../../common/user'
@ -17,7 +16,7 @@ export function ContractActivity(props: {
bets: Bet[]
comments: Comment[]
user: User | null | undefined
mode: 'only-recent' | 'abbreviated' | 'all'
mode: 'only-recent' | 'abbreviated' | 'all' | 'comments' | 'bets'
contractPath?: string
className?: string
betRowClassName?: string
@ -39,7 +38,12 @@ export function ContractActivity(props: {
? getRecentContractActivityItems(contract, bets, comments, user, {
contractPath,
})
: getAllContractActivityItems(contract, bets, comments, user, {
: mode === 'comments' || mode === 'bets'
? getSpecificContractActivityItems(contract, bets, comments, user, {
mode,
})
: // only used in abbreviated mode with folds/communities, all mode isn't used
getAllContractActivityItems(contract, bets, comments, user, {
abbreviated: mode === 'abbreviated',
})

View File

@ -1,5 +1,5 @@
// From https://tailwindui.com/components/application-ui/lists/feeds
import { Fragment, useRef, useState } from 'react'
import React, { Fragment, useRef, useState } from 'react'
import * as _ from 'lodash'
import {
BanIcon,
@ -67,7 +67,12 @@ export function FeedItems(props: {
<div className={clsx('flow-root pr-2 md:pr-0', className)} ref={ref}>
<div className={clsx(tradingAllowed(contract) ? '' : '-mb-6')}>
{items.map((item, activityItemIdx) => (
<div key={item.id} className="relative pb-6">
<div
key={item.id}
className={
item.type === 'answer' ? 'relative pb-2' : 'relative pb-6'
}
>
{activityItemIdx !== items.length - 1 ||
item.type === 'answergroup' ? (
<span
@ -104,6 +109,8 @@ function FeedItem(props: { item: ActivityItem }) {
return <FeedBetGroup {...item} />
case 'answergroup':
return <FeedAnswerGroup {...item} />
case 'answer':
return <FeedAnswerGroup {...item} />
case 'close':
return <FeedClose {...item} />
case 'resolve':
@ -184,35 +191,12 @@ function RelativeTimestamp(props: { time: number }) {
)
}
export function CommentInput(props: {
contract: Contract
commentsByBetId: Record<string, Comment>
bets: Bet[]
}) {
export function CommentInput(props: { contract: Contract }) {
// see if we can comment input on any bet:
const { contract, bets, commentsByBetId } = props
const { outcomeType } = contract
const { contract } = props
const user = useUser()
const [comment, setComment] = useState('')
if (outcomeType === 'FREE_RESPONSE') {
return <div />
}
let canCommentOnABet = false
bets.some((bet) => {
// make sure there is not already a comment with a matching bet id:
const matchingComment = commentsByBetId[bet.id]
if (matchingComment) {
return false
}
const { createdTime, userId } = bet
canCommentOnABet = canCommentOnBet(userId, createdTime, user)
return canCommentOnABet
})
if (canCommentOnABet) return <div />
async function submitComment() {
if (!comment) return
if (!user) {
@ -224,6 +208,7 @@ export function CommentInput(props: {
return (
<>
<Row className={'flex w-full gap-2 pt-5'}>
<div>
<Avatar avatarUrl={user?.avatarUrl} username={user?.username} />
</div>
@ -244,14 +229,17 @@ export function CommentInput(props: {
}}
/>
<button
className="btn btn-outline btn-sm mt-1"
className={
'btn btn-outline btn-sm text-transform: mt-1 capitalize'
}
onClick={submitComment}
>
Comment
{user ? 'Comment' : 'Sign in to comment'}
</button>
</div>
</div>
</div>
</Row>
</>
)
}
@ -335,7 +323,7 @@ export function FeedBet(props: {
}}
/>
<button
className="btn btn-outline btn-sm mt-1"
className="btn btn-outline btn-sm text-transform: mt-1 capitalize"
onClick={submitComment}
disabled={!canComment}
>
@ -644,8 +632,9 @@ function FeedAnswerGroup(props: {
contract: FullContract<any, FreeResponse>
answer: Answer
items: ActivityItem[]
type: string
}) {
const { answer, items, contract } = props
const { answer, items, contract, type } = props
const { username, avatarUrl, name, text } = answer
const prob = getDpmOutcomeProbability(contract.totalShares, answer.id)
@ -653,7 +642,13 @@ function FeedAnswerGroup(props: {
const [open, setOpen] = useState(false)
return (
<Col className="flex-1 gap-2">
<Col
className={
type === 'answer'
? 'border-base-200 bg-base-200 flex-1 rounded-md p-3'
: 'flex-1 gap-2'
}
>
<Modal open={open} setOpen={setOpen}>
<AnswerBetPanel
answer={answer}

View File

@ -27,6 +27,7 @@ export function OutcomeLabel(props: {
contract={contract as FullContract<DPM, FreeResponse>}
resolution={outcome}
truncate={truncate}
answerClassName={'font-bold text-base-400'}
/>
)
}

View File

@ -4,8 +4,11 @@ export function ProbabilitySelector(props: {
probabilityInt: number
setProbabilityInt: (p: number) => void
isSubmitting?: boolean
minProb?: number
maxProb?: number
}) {
const { probabilityInt, setProbabilityInt, isSubmitting } = props
const { probabilityInt, setProbabilityInt, isSubmitting, minProb, maxProb } =
props
return (
<Row className="items-center gap-2">
@ -15,19 +18,28 @@ export function ProbabilitySelector(props: {
value={probabilityInt}
className="input input-bordered input-md text-lg"
disabled={isSubmitting}
min={1}
max={99}
min={minProb ?? 1}
max={maxProb ?? 99}
onChange={(e) =>
setProbabilityInt(parseInt(e.target.value.substring(0, 2)))
}
onBlur={() =>
setProbabilityInt(
maxProb && probabilityInt > maxProb
? maxProb
: minProb && probabilityInt < minProb
? minProb
: probabilityInt
)
}
/>
<span>%</span>
</label>
<input
type="range"
className="range range-primary"
min={1}
max={99}
min={minProb ?? 1}
max={maxProb ?? 99}
value={probabilityInt}
onChange={(e) => setProbabilityInt(parseInt(e.target.value))}
/>

View File

@ -151,6 +151,10 @@ function getContractsActivityScores(
const activityCountScore =
0.5 + 0.5 * logInterpolation(0, 200, activtyCount)
const { volume7Days, volume } = contract
const combinedVolume = Math.log(volume7Days + 1) + Math.log(volume + 1)
const volumeScore = 0.5 + 0.5 * logInterpolation(4, 25, combinedVolume)
const lastBetTime =
contractMostRecentBet[contract.id]?.createdTime ?? contract.createdTime
const timeSinceLastBet = Date.now() - lastBetTime
@ -169,7 +173,11 @@ function getContractsActivityScores(
const probScore = 0.5 + frac * 0.5
const score =
newCommentScore * activityCountScore * timeAgoScore * probScore
newCommentScore *
activityCountScore *
volumeScore *
timeAgoScore *
probScore
// Map score to [0.5, 1] since no recent activty is not a deal breaker.
const mappedScore = 0.5 + score / 2

View File

@ -209,7 +209,7 @@ function ContractLeaderboard(props: { contract: Contract; bets: Bet[] }) {
return users && users.length > 0 ? (
<Leaderboard
title="🏅 Top traders"
title="🏅 Top bettors"
users={users || []}
columns={[
{

View File

@ -106,11 +106,14 @@ export function NewContract(props: { question: string; tag?: string }) {
setIsSubmitting(true)
const boundedProb =
initialProb > 90 ? 90 : initialProb < 10 ? 10 : initialProb
const result: any = await createContract({
question,
outcomeType,
description,
initialProb,
initialProb: boundedProb,
ante,
closeTime,
tags,
@ -172,6 +175,8 @@ export function NewContract(props: { question: string; tag?: string }) {
<ProbabilitySelector
probabilityInt={initialProb}
setProbabilityInt={setInitialProb}
minProb={10}
maxProb={90}
/>
</div>
)}

View File

@ -363,7 +363,7 @@ function FoldLeaderboards(props: {
<>
<Leaderboard
className="max-w-xl"
title="🏅 Top traders"
title="🏅 Top bettors"
users={topTraders}
columns={[
{

View File

@ -38,7 +38,7 @@ export default function Leaderboards(props: {
<Page margin>
<Col className="items-center gap-10 lg:flex-row">
<Leaderboard
title="🏅 Top traders"
title="🏅 Top bettors"
users={topTraders}
columns={[
{

View File

@ -1,20 +1,26 @@
import _ from 'lodash'
import { GetServerSideProps } from 'next'
import { getServerSideSitemap } from 'next-sitemap'
import { getServerSideSitemap, ISitemapField } from 'next-sitemap'
import { DOMAIN } from '../../common/envs/constants'
import { LiteMarket } from './api/v0/_types'
export const getServerSideProps: GetServerSideProps = async (ctx) => {
// Fetching data from https://docs.manifold.markets/api
// Fetching data from https://manifold.markets/api
const response = await fetch(`https://${DOMAIN}/api/v0/markets`)
const liteMarkets = await response.json()
const fields = liteMarkets.map((liteMarket: any) => ({
const liteMarkets = (await response.json()) as LiteMarket[]
const sortedMarkets = _.sortBy(liteMarkets, (m) => -m.volume24Hours)
const fields = sortedMarkets.map((market) => ({
// See https://www.sitemaps.org/protocol.html
loc: liteMarket.url,
changefreq: 'hourly',
priority: 0.2, // Individual markets aren't that important
loc: market.url,
changefreq: market.volume24Hours > 10 ? 'hourly' : 'daily',
priority: market.volume24Hours + market.volume7Days > 100 ? 0.7 : 0.1,
// TODO: Add `lastmod` aka last modified time
}))
return getServerSideSitemap(ctx, fields)
})) as ISitemapField[]
return await getServerSideSitemap(ctx, fields)
}
// Default export to prevent next.js errors

View File

@ -6,5 +6,5 @@ Allow: /
Host: https://manifold.markets
# Sitemaps
Sitemap: https://manifold.markets/sitemap.xml
Sitemap: https://manifold.markets/server-sitemap.xml
Sitemap: https://manifold.markets/sitemap.xml

View File

@ -1,19 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:news="http://www.google.com/schemas/sitemap-news/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml" xmlns:mobile="http://www.google.com/schemas/sitemap-mobile/1.0" xmlns:image="http://www.google.com/schemas/sitemap-image/1.1" xmlns:video="http://www.google.com/schemas/sitemap-video/1.1">
<url><loc>https://manifold.markets</loc><changefreq>hourly</changefreq><priority>0.7</priority><lastmod>2022-03-24T16:51:19.526Z</lastmod></url>
<url><loc>https://manifold.markets/about</loc><changefreq>hourly</changefreq><priority>0.7</priority><lastmod>2022-03-24T16:51:19.526Z</lastmod></url>
<url><loc>https://manifold.markets/account</loc><changefreq>hourly</changefreq><priority>0.7</priority><lastmod>2022-03-24T16:51:19.526Z</lastmod></url>
<url><loc>https://manifold.markets/add-funds</loc><changefreq>hourly</changefreq><priority>0.7</priority><lastmod>2022-03-24T16:51:19.526Z</lastmod></url>
<url><loc>https://manifold.markets/analytics</loc><changefreq>hourly</changefreq><priority>0.7</priority><lastmod>2022-03-24T16:51:19.526Z</lastmod></url>
<url><loc>https://manifold.markets/create</loc><changefreq>hourly</changefreq><priority>0.7</priority><lastmod>2022-03-24T16:51:19.526Z</lastmod></url>
<url><loc>https://manifold.markets/embed/analytics</loc><changefreq>hourly</changefreq><priority>0.7</priority><lastmod>2022-03-24T16:51:19.526Z</lastmod></url>
<url><loc>https://manifold.markets/folds</loc><changefreq>hourly</changefreq><priority>0.7</priority><lastmod>2022-03-24T16:51:19.526Z</lastmod></url>
<url><loc>https://manifold.markets/home</loc><changefreq>hourly</changefreq><priority>0.7</priority><lastmod>2022-03-24T16:51:19.526Z</lastmod></url>
<url><loc>https://manifold.markets/landing-page</loc><changefreq>hourly</changefreq><priority>0.7</priority><lastmod>2022-03-24T16:51:19.526Z</lastmod></url>
<url><loc>https://manifold.markets/leaderboards</loc><changefreq>hourly</changefreq><priority>0.7</priority><lastmod>2022-03-24T16:51:19.526Z</lastmod></url>
<url><loc>https://manifold.markets/make-predictions</loc><changefreq>hourly</changefreq><priority>0.7</priority><lastmod>2022-03-24T16:51:19.526Z</lastmod></url>
<url><loc>https://manifold.markets/markets</loc><changefreq>hourly</changefreq><priority>0.7</priority><lastmod>2022-03-24T16:51:19.526Z</lastmod></url>
<url><loc>https://manifold.markets/profile</loc><changefreq>hourly</changefreq><priority>0.7</priority><lastmod>2022-03-24T16:51:19.526Z</lastmod></url>
<url><loc>https://manifold.markets/simulator</loc><changefreq>hourly</changefreq><priority>0.7</priority><lastmod>2022-03-24T16:51:19.526Z</lastmod></url>
<url><loc>https://manifold.markets/portfolio</loc><changefreq>hourly</changefreq><priority>0.7</priority><lastmod>2022-03-24T16:51:19.526Z</lastmod></url>
<url><loc>https://manifold.markets</loc><changefreq>hourly</changefreq><priority>1.0</priority></url>
<url><loc>https://manifold.markets/markets</loc><changefreq>hourly</changefreq><priority>0.2</priority></url>
<url><loc>https://manifold.markets/leaderboards</loc><changefreq>daily</changefreq><priority>0.2</priority></url>
</urlset>