Merge branch 'main' into CPM-ui
This commit is contained in:
commit
d55cf571c5
53
README.md
53
README.md
|
@ -1,14 +1,51 @@
|
||||||
# Manifold Markets
|
# 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
|
## Directory overview
|
||||||
|
|
||||||
- `web/`: UI and biz logic. Where most of the site lives
|
- `web/`: UI and business logic for the client. Where most of the site lives. The public API endpoints are also in here.
|
||||||
- `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
|
|
||||||
|
|
||||||
## 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
|
- `common/`: Typescript library code shared between `web/` & `functions/`. If you want to look at how the market math
|
||||||
- [Monorepo](https://semaphoreci.com/blog/what-is-monorepo): Good fit for our current size
|
works, most of that's in here (it gets called from the `placeBet` and `sellBet` endpoints in `functions/`.) Also
|
||||||
- [Small PRs](https://google.github.io/eng-practices/review/developer/small-cls.html): Lots of little changes > one big diff
|
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
|
||||||
|
|
|
@ -39,16 +39,24 @@ service cloud.firestore {
|
||||||
allow update: if request.resource.data.diff(resource.data).affectedKeys()
|
allow update: if request.resource.data.diff(resource.data).affectedKeys()
|
||||||
.hasOnly(['description', 'closeTime', 'tags', 'lowercaseTags']);
|
.hasOnly(['description', 'closeTime', 'tags', 'lowercaseTags']);
|
||||||
allow update: if isAdmin();
|
allow update: if isAdmin();
|
||||||
allow delete: if resource.data.creatorId == request.auth.uid;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
match /{somePath=**}/bets/{betId} {
|
match /{somePath=**}/bets/{betId} {
|
||||||
allow read;
|
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} {
|
match /{somePath=**}/comments/{commentId} {
|
||||||
allow read;
|
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} {
|
match /{somePath=**}/answers/{answerId} {
|
||||||
|
@ -57,12 +65,16 @@ service cloud.firestore {
|
||||||
|
|
||||||
match /folds/{foldId} {
|
match /folds/{foldId} {
|
||||||
allow read;
|
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} {
|
match /{somePath=**}/followers/{userId} {
|
||||||
allow read;
|
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} {
|
match /txns/{txnId} {
|
||||||
|
|
|
@ -34,8 +34,9 @@ Adapted from https://firebase.google.com/docs/functions/get-started
|
||||||
|
|
||||||
## Developing locally
|
## Developing locally
|
||||||
|
|
||||||
|
0. `$ firebase use dev` if you haven't already
|
||||||
1. `$ yarn serve` to spin up the emulators
|
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 =(
|
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
|
2. `$ yarn dev:emulate` in `/web` to connect to emulators with the frontend
|
||||||
1. Note: emulated database is cleared after every shutdown
|
1. Note: emulated database is cleared after every shutdown
|
||||||
|
|
|
@ -48,8 +48,8 @@ export const sendMarketResolutionEmail = async (
|
||||||
creatorName: creator.name,
|
creatorName: creator.name,
|
||||||
question: contract.question,
|
question: contract.question,
|
||||||
outcome,
|
outcome,
|
||||||
investment: `${Math.round(investment)}`,
|
investment: `${Math.floor(investment)}`,
|
||||||
payout: `${Math.round(payout)}`,
|
payout: `${Math.floor(payout)}`,
|
||||||
url: `https://${DOMAIN}/${creator.username}/${contract.slug}`,
|
url: `https://${DOMAIN}/${creator.username}/${contract.slug}`,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -189,7 +189,9 @@ export const sendNewCommentEmail = async (
|
||||||
let betDescription = ''
|
let betDescription = ''
|
||||||
if (bet) {
|
if (bet) {
|
||||||
const { amount, sale } = 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}`
|
const subject = `Comment on ${question}`
|
||||||
|
|
|
@ -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
|
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
|
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
|
Manifold's website uses [Next.js][nextjs], which is a [React][react]-based framework that handles concerns like routing,
|
||||||
2. `yarn dev:emulate` will point you to the emulated database
|
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
|
## Developer Experience TODOs
|
||||||
|
|
||||||
- Prevent git pushing if there are Typescript errors?
|
- 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
|
||||||
|
|
|
@ -11,6 +11,11 @@ import { AnswerItem } from './answer-item'
|
||||||
import { CreateAnswerPanel } from './create-answer-panel'
|
import { CreateAnswerPanel } from './create-answer-panel'
|
||||||
import { AnswerResolvePanel } from './answer-resolve-panel'
|
import { AnswerResolvePanel } from './answer-resolve-panel'
|
||||||
import { Spacer } from '../layout/spacer'
|
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: {
|
export function AnswersPanel(props: {
|
||||||
contract: FullContract<DPM, FreeResponse>
|
contract: FullContract<DPM, FreeResponse>
|
||||||
|
@ -47,6 +52,8 @@ export function AnswersPanel(props: {
|
||||||
|
|
||||||
const chosenTotal = _.sum(Object.values(chosenAnswers))
|
const chosenTotal = _.sum(Object.values(chosenAnswers))
|
||||||
|
|
||||||
|
const answerItems = getAnswers(contract, user)
|
||||||
|
|
||||||
const onChoose = (answerId: string, prob: number) => {
|
const onChoose = (answerId: string, prob: number) => {
|
||||||
if (resolveOption === 'CHOOSE') {
|
if (resolveOption === 'CHOOSE') {
|
||||||
setChosenAnswers({ [answerId]: prob })
|
setChosenAnswers({ [answerId]: prob })
|
||||||
|
@ -102,6 +109,15 @@ export function AnswersPanel(props: {
|
||||||
<div className="pb-4 text-gray-500">No answers yet...</div>
|
<div className="pb-4 text-gray-500">No answers yet...</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{!resolveOption && sortedAnswers.length > 0 && (
|
||||||
|
<FeedItems
|
||||||
|
contract={contract}
|
||||||
|
items={answerItems}
|
||||||
|
className={''}
|
||||||
|
betRowClassName={''}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{tradingAllowed(contract) &&
|
{tradingAllowed(contract) &&
|
||||||
(!resolveOption || resolveOption === 'CANCEL') && (
|
(!resolveOption || resolveOption === 'CANCEL') && (
|
||||||
<CreateAnswerPanel contract={contract} />
|
<CreateAnswerPanel contract={contract} />
|
||||||
|
@ -121,3 +137,32 @@ export function AnswersPanel(props: {
|
||||||
</Col>
|
</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)
|
||||||
|
}
|
||||||
|
|
|
@ -21,13 +21,24 @@ export function ContractTabs(props: {
|
||||||
bets.sort((bet1, bet2) => bet2.createdTime - bet1.createdTime)
|
bets.sort((bet1, bet2) => bet2.createdTime - bet1.createdTime)
|
||||||
const userBets = user && bets.filter((bet) => bet.userId === user.id)
|
const userBets = user && bets.filter((bet) => bet.userId === user.id)
|
||||||
|
|
||||||
const activity = (
|
const betActivity = (
|
||||||
<ContractActivity
|
<ContractActivity
|
||||||
contract={contract}
|
contract={contract}
|
||||||
bets={bets}
|
bets={bets}
|
||||||
comments={comments}
|
comments={comments}
|
||||||
user={user}
|
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"
|
betRowClassName="!mt-0 xl:hidden"
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
@ -48,10 +59,11 @@ export function ContractTabs(props: {
|
||||||
return (
|
return (
|
||||||
<Tabs
|
<Tabs
|
||||||
tabs={[
|
tabs={[
|
||||||
{ title: 'Timeline', content: activity },
|
{ title: 'Comments', content: commentActivity },
|
||||||
|
{ title: 'Bets', content: betActivity },
|
||||||
...(!user || !userBets?.length
|
...(!user || !userBets?.length
|
||||||
? []
|
? []
|
||||||
: [{ title: 'Your trades', content: yourTrades }]),
|
: [{ title: 'Your bets', content: yourTrades }]),
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|
|
@ -25,15 +25,16 @@ export function ContractsGrid(props: {
|
||||||
showCloseTime?: boolean
|
showCloseTime?: boolean
|
||||||
}) {
|
}) {
|
||||||
const { showCloseTime } = props
|
const { showCloseTime } = props
|
||||||
|
const PAGE_SIZE = 100
|
||||||
|
const [page, setPage] = useState(1)
|
||||||
|
|
||||||
const [resolvedContracts, activeContracts] = _.partition(
|
const [resolvedContracts, activeContracts] = _.partition(
|
||||||
props.contracts,
|
props.contracts,
|
||||||
(c) => c.isResolved
|
(c) => c.isResolved
|
||||||
)
|
)
|
||||||
const contracts = [...activeContracts, ...resolvedContracts].slice(
|
const allContracts = [...activeContracts, ...resolvedContracts]
|
||||||
0,
|
const showMore = allContracts.length > PAGE_SIZE * page
|
||||||
MAX_CONTRACTS_DISPLAYED
|
const contracts = allContracts.slice(0, PAGE_SIZE * page)
|
||||||
)
|
|
||||||
|
|
||||||
if (contracts.length === 0) {
|
if (contracts.length === 0) {
|
||||||
return (
|
return (
|
||||||
|
@ -47,6 +48,7 @@ export function ContractsGrid(props: {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
<ul className="grid w-full grid-cols-1 gap-6 md:grid-cols-2">
|
<ul className="grid w-full grid-cols-1 gap-6 md:grid-cols-2">
|
||||||
{contracts.map((contract) => (
|
{contracts.map((contract) => (
|
||||||
<ContractCard
|
<ContractCard
|
||||||
|
@ -57,6 +59,16 @@ export function ContractsGrid(props: {
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</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>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -31,8 +31,6 @@ type BaseActivityItem = {
|
||||||
|
|
||||||
export type CommentInputItem = BaseActivityItem & {
|
export type CommentInputItem = BaseActivityItem & {
|
||||||
type: 'commentInput'
|
type: 'commentInput'
|
||||||
bets: Bet[]
|
|
||||||
commentsByBetId: Record<string, Comment>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type DescriptionItem = BaseActivityItem & {
|
export type DescriptionItem = BaseActivityItem & {
|
||||||
|
@ -68,7 +66,7 @@ export type BetGroupItem = BaseActivityItem & {
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AnswerGroupItem = BaseActivityItem & {
|
export type AnswerGroupItem = BaseActivityItem & {
|
||||||
type: 'answergroup'
|
type: 'answergroup' | 'answer'
|
||||||
answer: Answer
|
answer: Answer
|
||||||
items: ActivityItem[]
|
items: ActivityItem[]
|
||||||
}
|
}
|
||||||
|
@ -82,6 +80,7 @@ export type ResolveItem = BaseActivityItem & {
|
||||||
}
|
}
|
||||||
|
|
||||||
const DAY_IN_MS = 24 * 60 * 60 * 1000
|
const DAY_IN_MS = 24 * 60 * 60 * 1000
|
||||||
|
const ABBREVIATED_NUM_COMMENTS_OR_BETS_TO_SHOW = 3
|
||||||
|
|
||||||
// Group together bets that are:
|
// Group together bets that are:
|
||||||
// - Within a day of the first in the group
|
// - Within a day of the first in the group
|
||||||
|
@ -173,7 +172,9 @@ function groupBets(
|
||||||
if (group.length > 0) {
|
if (group.length > 0) {
|
||||||
pushGroup()
|
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()
|
if (reversed) abbrItems.reverse()
|
||||||
return abbrItems
|
return abbrItems
|
||||||
}
|
}
|
||||||
|
@ -240,7 +241,8 @@ function getAnswerGroups(
|
||||||
reversed,
|
reversed,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (abbreviated) items = items.slice(-2)
|
if (abbreviated)
|
||||||
|
items = items.slice(-ABBREVIATED_NUM_COMMENTS_OR_BETS_TO_SHOW)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: outcome,
|
id: outcome,
|
||||||
|
@ -253,6 +255,8 @@ function getAnswerGroups(
|
||||||
})
|
})
|
||||||
.filter((group) => group.answer)
|
.filter((group) => group.answer)
|
||||||
|
|
||||||
|
if (reversed) answerGroups.reverse()
|
||||||
|
|
||||||
return answerGroups
|
return answerGroups
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -268,6 +272,7 @@ function groupBetsAndComments(
|
||||||
reversed: boolean
|
reversed: boolean
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
|
const { smallAvatar, abbreviated, reversed } = options
|
||||||
const commentsWithoutBets = comments
|
const commentsWithoutBets = comments
|
||||||
.filter((comment) => !comment.betId)
|
.filter((comment) => !comment.betId)
|
||||||
.map((comment) => ({
|
.map((comment) => ({
|
||||||
|
@ -276,16 +281,16 @@ function groupBetsAndComments(
|
||||||
contract: contract,
|
contract: contract,
|
||||||
comment,
|
comment,
|
||||||
bet: undefined,
|
bet: undefined,
|
||||||
truncate: false,
|
truncate: abbreviated,
|
||||||
hideOutcome: true,
|
hideOutcome: true,
|
||||||
smallAvatar: false,
|
smallAvatar,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const groupedBets = groupBets(bets, comments, contract, userId, options)
|
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:
|
// 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 unorderedBetsAndComments = [...commentsWithoutBets, ...groupedBets]
|
||||||
const sortedBetsAndComments = _.sortBy(unorderedBetsAndComments, (item) => {
|
let sortedBetsAndComments = _.sortBy(unorderedBetsAndComments, (item) => {
|
||||||
if (item.type === 'comment') {
|
if (item.type === 'comment') {
|
||||||
return item.comment.createdTime
|
return item.comment.createdTime
|
||||||
} else if (item.type === 'bet') {
|
} else if (item.type === 'bet') {
|
||||||
|
@ -294,7 +299,13 @@ function groupBetsAndComments(
|
||||||
return item.bets[0].createdTime
|
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(
|
export function getAllContractActivityItems(
|
||||||
|
@ -308,8 +319,7 @@ export function getAllContractActivityItems(
|
||||||
) {
|
) {
|
||||||
const { abbreviated } = options
|
const { abbreviated } = options
|
||||||
const { outcomeType } = contract
|
const { outcomeType } = contract
|
||||||
|
const reversed = true
|
||||||
const reversed = !abbreviated
|
|
||||||
|
|
||||||
bets =
|
bets =
|
||||||
outcomeType === 'BINARY'
|
outcomeType === 'BINARY'
|
||||||
|
@ -328,19 +338,30 @@ export function getAllContractActivityItems(
|
||||||
: [{ type: 'description', id: '0', contract }]
|
: [{ type: 'description', id: '0', contract }]
|
||||||
|
|
||||||
if (outcomeType === 'FREE_RESPONSE') {
|
if (outcomeType === 'FREE_RESPONSE') {
|
||||||
|
const onlyUsersBetsOrBetsWithComments = bets.filter((bet) =>
|
||||||
|
comments.some(
|
||||||
|
(comment) => comment.betId === bet.id || bet.userId === user?.id
|
||||||
|
)
|
||||||
|
)
|
||||||
items.push(
|
items.push(
|
||||||
...getAnswerGroups(
|
...groupBetsAndComments(
|
||||||
contract as FullContract<DPM, FreeResponse>,
|
onlyUsersBetsOrBetsWithComments,
|
||||||
bets,
|
|
||||||
comments,
|
comments,
|
||||||
user,
|
contract,
|
||||||
|
user?.id,
|
||||||
{
|
{
|
||||||
sortByProb: true,
|
hideOutcome: false,
|
||||||
abbreviated,
|
abbreviated,
|
||||||
|
smallAvatar: false,
|
||||||
reversed,
|
reversed,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
items.push({
|
||||||
|
type: 'commentInput',
|
||||||
|
id: 'commentInput',
|
||||||
|
contract,
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
items.push(
|
items.push(
|
||||||
...groupBetsAndComments(bets, comments, contract, user?.id, {
|
...groupBetsAndComments(bets, comments, contract, user?.id, {
|
||||||
|
@ -359,14 +380,13 @@ export function getAllContractActivityItems(
|
||||||
items.push({ type: 'resolve', id: `${contract.resolutionTime}`, contract })
|
items.push({ type: 'resolve', id: `${contract.resolutionTime}`, contract })
|
||||||
}
|
}
|
||||||
|
|
||||||
const commentsByBetId = mapCommentsByBetId(comments)
|
if (outcomeType === 'BINARY') {
|
||||||
items.push({
|
items.push({
|
||||||
type: 'commentInput',
|
type: 'commentInput',
|
||||||
id: 'commentInput',
|
id: 'commentInput',
|
||||||
bets,
|
|
||||||
commentsByBetId,
|
|
||||||
contract,
|
contract,
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
if (reversed) items.reverse()
|
if (reversed) items.reverse()
|
||||||
|
|
||||||
|
@ -396,9 +416,10 @@ export function getRecentContractActivityItems(
|
||||||
contractPath,
|
contractPath,
|
||||||
}
|
}
|
||||||
|
|
||||||
const items =
|
const items = []
|
||||||
contract.outcomeType === 'FREE_RESPONSE'
|
if (contract.outcomeType === 'FREE_RESPONSE') {
|
||||||
? getAnswerGroups(
|
items.push(
|
||||||
|
...getAnswerGroups(
|
||||||
contract as FullContract<DPM, FreeResponse>,
|
contract as FullContract<DPM, FreeResponse>,
|
||||||
bets,
|
bets,
|
||||||
comments,
|
comments,
|
||||||
|
@ -406,15 +427,84 @@ export function getRecentContractActivityItems(
|
||||||
{
|
{
|
||||||
sortByProb: false,
|
sortByProb: false,
|
||||||
abbreviated: true,
|
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,
|
hideOutcome: false,
|
||||||
abbreviated: true,
|
abbreviated: true,
|
||||||
smallAvatar: false,
|
smallAvatar: false,
|
||||||
reversed: false,
|
reversed: true,
|
||||||
})
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return [questionItem, ...items]
|
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()
|
||||||
|
}
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
import _ from 'lodash'
|
|
||||||
|
|
||||||
import { Contract } from '../../lib/firebase/contracts'
|
import { Contract } from '../../lib/firebase/contracts'
|
||||||
import { Comment } from '../../lib/firebase/comments'
|
import { Comment } from '../../lib/firebase/comments'
|
||||||
import { Bet } from '../../../common/bet'
|
import { Bet } from '../../../common/bet'
|
||||||
|
@ -8,6 +6,7 @@ import { useComments } from '../../hooks/use-comments'
|
||||||
import {
|
import {
|
||||||
getAllContractActivityItems,
|
getAllContractActivityItems,
|
||||||
getRecentContractActivityItems,
|
getRecentContractActivityItems,
|
||||||
|
getSpecificContractActivityItems,
|
||||||
} from './activity-items'
|
} from './activity-items'
|
||||||
import { FeedItems } from './feed-items'
|
import { FeedItems } from './feed-items'
|
||||||
import { User } from '../../../common/user'
|
import { User } from '../../../common/user'
|
||||||
|
@ -17,7 +16,7 @@ export function ContractActivity(props: {
|
||||||
bets: Bet[]
|
bets: Bet[]
|
||||||
comments: Comment[]
|
comments: Comment[]
|
||||||
user: User | null | undefined
|
user: User | null | undefined
|
||||||
mode: 'only-recent' | 'abbreviated' | 'all'
|
mode: 'only-recent' | 'abbreviated' | 'all' | 'comments' | 'bets'
|
||||||
contractPath?: string
|
contractPath?: string
|
||||||
className?: string
|
className?: string
|
||||||
betRowClassName?: string
|
betRowClassName?: string
|
||||||
|
@ -39,7 +38,12 @@ export function ContractActivity(props: {
|
||||||
? getRecentContractActivityItems(contract, bets, comments, user, {
|
? getRecentContractActivityItems(contract, bets, comments, user, {
|
||||||
contractPath,
|
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',
|
abbreviated: mode === 'abbreviated',
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
// From https://tailwindui.com/components/application-ui/lists/feeds
|
// 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 * as _ from 'lodash'
|
||||||
import {
|
import {
|
||||||
BanIcon,
|
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('flow-root pr-2 md:pr-0', className)} ref={ref}>
|
||||||
<div className={clsx(tradingAllowed(contract) ? '' : '-mb-6')}>
|
<div className={clsx(tradingAllowed(contract) ? '' : '-mb-6')}>
|
||||||
{items.map((item, activityItemIdx) => (
|
{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 ||
|
{activityItemIdx !== items.length - 1 ||
|
||||||
item.type === 'answergroup' ? (
|
item.type === 'answergroup' ? (
|
||||||
<span
|
<span
|
||||||
|
@ -104,6 +109,8 @@ function FeedItem(props: { item: ActivityItem }) {
|
||||||
return <FeedBetGroup {...item} />
|
return <FeedBetGroup {...item} />
|
||||||
case 'answergroup':
|
case 'answergroup':
|
||||||
return <FeedAnswerGroup {...item} />
|
return <FeedAnswerGroup {...item} />
|
||||||
|
case 'answer':
|
||||||
|
return <FeedAnswerGroup {...item} />
|
||||||
case 'close':
|
case 'close':
|
||||||
return <FeedClose {...item} />
|
return <FeedClose {...item} />
|
||||||
case 'resolve':
|
case 'resolve':
|
||||||
|
@ -184,35 +191,12 @@ function RelativeTimestamp(props: { time: number }) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CommentInput(props: {
|
export function CommentInput(props: { contract: Contract }) {
|
||||||
contract: Contract
|
|
||||||
commentsByBetId: Record<string, Comment>
|
|
||||||
bets: Bet[]
|
|
||||||
}) {
|
|
||||||
// see if we can comment input on any bet:
|
// see if we can comment input on any bet:
|
||||||
const { contract, bets, commentsByBetId } = props
|
const { contract } = props
|
||||||
const { outcomeType } = contract
|
|
||||||
const user = useUser()
|
const user = useUser()
|
||||||
const [comment, setComment] = useState('')
|
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() {
|
async function submitComment() {
|
||||||
if (!comment) return
|
if (!comment) return
|
||||||
if (!user) {
|
if (!user) {
|
||||||
|
@ -224,6 +208,7 @@ export function CommentInput(props: {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<Row className={'flex w-full gap-2 pt-5'}>
|
||||||
<div>
|
<div>
|
||||||
<Avatar avatarUrl={user?.avatarUrl} username={user?.username} />
|
<Avatar avatarUrl={user?.avatarUrl} username={user?.username} />
|
||||||
</div>
|
</div>
|
||||||
|
@ -244,14 +229,17 @@ export function CommentInput(props: {
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
className="btn btn-outline btn-sm mt-1"
|
className={
|
||||||
|
'btn btn-outline btn-sm text-transform: mt-1 capitalize'
|
||||||
|
}
|
||||||
onClick={submitComment}
|
onClick={submitComment}
|
||||||
>
|
>
|
||||||
Comment
|
{user ? 'Comment' : 'Sign in to comment'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</Row>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -335,7 +323,7 @@ export function FeedBet(props: {
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
className="btn btn-outline btn-sm mt-1"
|
className="btn btn-outline btn-sm text-transform: mt-1 capitalize"
|
||||||
onClick={submitComment}
|
onClick={submitComment}
|
||||||
disabled={!canComment}
|
disabled={!canComment}
|
||||||
>
|
>
|
||||||
|
@ -644,8 +632,9 @@ function FeedAnswerGroup(props: {
|
||||||
contract: FullContract<any, FreeResponse>
|
contract: FullContract<any, FreeResponse>
|
||||||
answer: Answer
|
answer: Answer
|
||||||
items: ActivityItem[]
|
items: ActivityItem[]
|
||||||
|
type: string
|
||||||
}) {
|
}) {
|
||||||
const { answer, items, contract } = props
|
const { answer, items, contract, type } = props
|
||||||
const { username, avatarUrl, name, text } = answer
|
const { username, avatarUrl, name, text } = answer
|
||||||
|
|
||||||
const prob = getDpmOutcomeProbability(contract.totalShares, answer.id)
|
const prob = getDpmOutcomeProbability(contract.totalShares, answer.id)
|
||||||
|
@ -653,7 +642,13 @@ function FeedAnswerGroup(props: {
|
||||||
const [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
|
|
||||||
return (
|
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}>
|
<Modal open={open} setOpen={setOpen}>
|
||||||
<AnswerBetPanel
|
<AnswerBetPanel
|
||||||
answer={answer}
|
answer={answer}
|
||||||
|
|
|
@ -27,6 +27,7 @@ export function OutcomeLabel(props: {
|
||||||
contract={contract as FullContract<DPM, FreeResponse>}
|
contract={contract as FullContract<DPM, FreeResponse>}
|
||||||
resolution={outcome}
|
resolution={outcome}
|
||||||
truncate={truncate}
|
truncate={truncate}
|
||||||
|
answerClassName={'font-bold text-base-400'}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,8 +4,11 @@ export function ProbabilitySelector(props: {
|
||||||
probabilityInt: number
|
probabilityInt: number
|
||||||
setProbabilityInt: (p: number) => void
|
setProbabilityInt: (p: number) => void
|
||||||
isSubmitting?: boolean
|
isSubmitting?: boolean
|
||||||
|
minProb?: number
|
||||||
|
maxProb?: number
|
||||||
}) {
|
}) {
|
||||||
const { probabilityInt, setProbabilityInt, isSubmitting } = props
|
const { probabilityInt, setProbabilityInt, isSubmitting, minProb, maxProb } =
|
||||||
|
props
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Row className="items-center gap-2">
|
<Row className="items-center gap-2">
|
||||||
|
@ -15,19 +18,28 @@ export function ProbabilitySelector(props: {
|
||||||
value={probabilityInt}
|
value={probabilityInt}
|
||||||
className="input input-bordered input-md text-lg"
|
className="input input-bordered input-md text-lg"
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
min={1}
|
min={minProb ?? 1}
|
||||||
max={99}
|
max={maxProb ?? 99}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setProbabilityInt(parseInt(e.target.value.substring(0, 2)))
|
setProbabilityInt(parseInt(e.target.value.substring(0, 2)))
|
||||||
}
|
}
|
||||||
|
onBlur={() =>
|
||||||
|
setProbabilityInt(
|
||||||
|
maxProb && probabilityInt > maxProb
|
||||||
|
? maxProb
|
||||||
|
: minProb && probabilityInt < minProb
|
||||||
|
? minProb
|
||||||
|
: probabilityInt
|
||||||
|
)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<span>%</span>
|
<span>%</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="range"
|
type="range"
|
||||||
className="range range-primary"
|
className="range range-primary"
|
||||||
min={1}
|
min={minProb ?? 1}
|
||||||
max={99}
|
max={maxProb ?? 99}
|
||||||
value={probabilityInt}
|
value={probabilityInt}
|
||||||
onChange={(e) => setProbabilityInt(parseInt(e.target.value))}
|
onChange={(e) => setProbabilityInt(parseInt(e.target.value))}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -151,6 +151,10 @@ function getContractsActivityScores(
|
||||||
const activityCountScore =
|
const activityCountScore =
|
||||||
0.5 + 0.5 * logInterpolation(0, 200, activtyCount)
|
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 =
|
const lastBetTime =
|
||||||
contractMostRecentBet[contract.id]?.createdTime ?? contract.createdTime
|
contractMostRecentBet[contract.id]?.createdTime ?? contract.createdTime
|
||||||
const timeSinceLastBet = Date.now() - lastBetTime
|
const timeSinceLastBet = Date.now() - lastBetTime
|
||||||
|
@ -169,7 +173,11 @@ function getContractsActivityScores(
|
||||||
const probScore = 0.5 + frac * 0.5
|
const probScore = 0.5 + frac * 0.5
|
||||||
|
|
||||||
const score =
|
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.
|
// Map score to [0.5, 1] since no recent activty is not a deal breaker.
|
||||||
const mappedScore = 0.5 + score / 2
|
const mappedScore = 0.5 + score / 2
|
||||||
|
|
|
@ -209,7 +209,7 @@ function ContractLeaderboard(props: { contract: Contract; bets: Bet[] }) {
|
||||||
|
|
||||||
return users && users.length > 0 ? (
|
return users && users.length > 0 ? (
|
||||||
<Leaderboard
|
<Leaderboard
|
||||||
title="🏅 Top traders"
|
title="🏅 Top bettors"
|
||||||
users={users || []}
|
users={users || []}
|
||||||
columns={[
|
columns={[
|
||||||
{
|
{
|
||||||
|
|
|
@ -106,11 +106,14 @@ export function NewContract(props: { question: string; tag?: string }) {
|
||||||
|
|
||||||
setIsSubmitting(true)
|
setIsSubmitting(true)
|
||||||
|
|
||||||
|
const boundedProb =
|
||||||
|
initialProb > 90 ? 90 : initialProb < 10 ? 10 : initialProb
|
||||||
|
|
||||||
const result: any = await createContract({
|
const result: any = await createContract({
|
||||||
question,
|
question,
|
||||||
outcomeType,
|
outcomeType,
|
||||||
description,
|
description,
|
||||||
initialProb,
|
initialProb: boundedProb,
|
||||||
ante,
|
ante,
|
||||||
closeTime,
|
closeTime,
|
||||||
tags,
|
tags,
|
||||||
|
@ -172,6 +175,8 @@ export function NewContract(props: { question: string; tag?: string }) {
|
||||||
<ProbabilitySelector
|
<ProbabilitySelector
|
||||||
probabilityInt={initialProb}
|
probabilityInt={initialProb}
|
||||||
setProbabilityInt={setInitialProb}
|
setProbabilityInt={setInitialProb}
|
||||||
|
minProb={10}
|
||||||
|
maxProb={90}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -363,7 +363,7 @@ function FoldLeaderboards(props: {
|
||||||
<>
|
<>
|
||||||
<Leaderboard
|
<Leaderboard
|
||||||
className="max-w-xl"
|
className="max-w-xl"
|
||||||
title="🏅 Top traders"
|
title="🏅 Top bettors"
|
||||||
users={topTraders}
|
users={topTraders}
|
||||||
columns={[
|
columns={[
|
||||||
{
|
{
|
||||||
|
|
|
@ -38,7 +38,7 @@ export default function Leaderboards(props: {
|
||||||
<Page margin>
|
<Page margin>
|
||||||
<Col className="items-center gap-10 lg:flex-row">
|
<Col className="items-center gap-10 lg:flex-row">
|
||||||
<Leaderboard
|
<Leaderboard
|
||||||
title="🏅 Top traders"
|
title="🏅 Top bettors"
|
||||||
users={topTraders}
|
users={topTraders}
|
||||||
columns={[
|
columns={[
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,20 +1,26 @@
|
||||||
|
import _ from 'lodash'
|
||||||
import { GetServerSideProps } from 'next'
|
import { GetServerSideProps } from 'next'
|
||||||
import { getServerSideSitemap } from 'next-sitemap'
|
import { getServerSideSitemap, ISitemapField } from 'next-sitemap'
|
||||||
|
|
||||||
import { DOMAIN } from '../../common/envs/constants'
|
import { DOMAIN } from '../../common/envs/constants'
|
||||||
|
import { LiteMarket } from './api/v0/_types'
|
||||||
|
|
||||||
export const getServerSideProps: GetServerSideProps = async (ctx) => {
|
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 response = await fetch(`https://${DOMAIN}/api/v0/markets`)
|
||||||
|
|
||||||
const liteMarkets = await response.json()
|
const liteMarkets = (await response.json()) as LiteMarket[]
|
||||||
const fields = liteMarkets.map((liteMarket: any) => ({
|
const sortedMarkets = _.sortBy(liteMarkets, (m) => -m.volume24Hours)
|
||||||
|
|
||||||
|
const fields = sortedMarkets.map((market) => ({
|
||||||
// See https://www.sitemaps.org/protocol.html
|
// See https://www.sitemaps.org/protocol.html
|
||||||
loc: liteMarket.url,
|
loc: market.url,
|
||||||
changefreq: 'hourly',
|
changefreq: market.volume24Hours > 10 ? 'hourly' : 'daily',
|
||||||
priority: 0.2, // Individual markets aren't that important
|
priority: market.volume24Hours + market.volume7Days > 100 ? 0.7 : 0.1,
|
||||||
// TODO: Add `lastmod` aka last modified time
|
// TODO: Add `lastmod` aka last modified time
|
||||||
}))
|
})) as ISitemapField[]
|
||||||
return getServerSideSitemap(ctx, fields)
|
|
||||||
|
return await getServerSideSitemap(ctx, fields)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default export to prevent next.js errors
|
// Default export to prevent next.js errors
|
||||||
|
|
|
@ -6,5 +6,5 @@ Allow: /
|
||||||
Host: https://manifold.markets
|
Host: https://manifold.markets
|
||||||
|
|
||||||
# Sitemaps
|
# Sitemaps
|
||||||
Sitemap: https://manifold.markets/sitemap.xml
|
|
||||||
Sitemap: https://manifold.markets/server-sitemap.xml
|
Sitemap: https://manifold.markets/server-sitemap.xml
|
||||||
|
Sitemap: https://manifold.markets/sitemap.xml
|
||||||
|
|
|
@ -1,19 +1,6 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?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">
|
<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</loc><changefreq>hourly</changefreq><priority>1.0</priority></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/markets</loc><changefreq>hourly</changefreq><priority>0.2</priority></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/leaderboards</loc><changefreq>daily</changefreq><priority>0.2</priority></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>
|
|
||||||
</urlset>
|
</urlset>
|
Loading…
Reference in New Issue
Block a user