Merge branch 'main' into user-profile
This commit is contained in:
commit
4d86fa2256
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,3 +1,4 @@
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
.vercel
|
.vercel
|
||||||
|
node_modules
|
||||||
|
|
13
common/.gitignore
vendored
Normal file
13
common/.gitignore
vendored
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
# Compiled JavaScript files
|
||||||
|
lib/**/*.js
|
||||||
|
lib/**/*.js.map
|
||||||
|
|
||||||
|
# TypeScript v1 declaration files
|
||||||
|
typings/
|
||||||
|
|
||||||
|
# Node.js dependency directory
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
package-lock.json
|
||||||
|
ui-debug.log
|
||||||
|
firebase-debug.log
|
|
@ -7,6 +7,7 @@ export type Fold = {
|
||||||
createdTime: number
|
createdTime: number
|
||||||
|
|
||||||
tags: string[]
|
tags: string[]
|
||||||
|
lowercaseTags: string[]
|
||||||
|
|
||||||
contractIds: string[]
|
contractIds: string[]
|
||||||
excludedContractIds: string[]
|
excludedContractIds: string[]
|
||||||
|
@ -17,4 +18,6 @@ export type Fold = {
|
||||||
excludedCreatorIds?: string[]
|
excludedCreatorIds?: string[]
|
||||||
|
|
||||||
followCount: number
|
followCount: number
|
||||||
|
|
||||||
|
disallowMarketCreation?: boolean
|
||||||
}
|
}
|
||||||
|
|
12
common/package.json
Normal file
12
common/package.json
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"name": "common",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {},
|
||||||
|
"dependencies": {
|
||||||
|
"lodash": "4.17.21"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/lodash": "4.14.178"
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
import _ from 'lodash'
|
import * as _ from 'lodash'
|
||||||
import { Contract } from '../../../common/contract'
|
import { Bet } from './bet'
|
||||||
import { getPayouts } from '../../../common/payouts'
|
import { Contract } from './contract'
|
||||||
import { Bet } from './bets'
|
import { getPayouts } from './payouts'
|
||||||
|
|
||||||
export function scoreCreators(contracts: Contract[], bets: Bet[][]) {
|
export function scoreCreators(contracts: Contract[], bets: Bet[][]) {
|
||||||
const creatorScore = _.mapValues(
|
const creatorScore = _.mapValues(
|
||||||
|
@ -18,15 +18,12 @@ export function scoreTraders(contracts: Contract[], bets: Bet[][]) {
|
||||||
)
|
)
|
||||||
const userScores: { [userId: string]: number } = {}
|
const userScores: { [userId: string]: number } = {}
|
||||||
for (const scores of userScoresByContract) {
|
for (const scores of userScoresByContract) {
|
||||||
for (const [userId, score] of Object.entries(scores)) {
|
addUserScores(scores, userScores)
|
||||||
if (userScores[userId] === undefined) userScores[userId] = 0
|
|
||||||
userScores[userId] += score
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return userScores
|
return userScores
|
||||||
}
|
}
|
||||||
|
|
||||||
function scoreUsersByContract(contract: Contract, bets: Bet[]) {
|
export function scoreUsersByContract(contract: Contract, bets: Bet[]) {
|
||||||
const { resolution, resolutionProbability } = contract
|
const { resolution, resolutionProbability } = contract
|
||||||
|
|
||||||
const [closedBets, openBets] = _.partition(
|
const [closedBets, openBets] = _.partition(
|
||||||
|
@ -61,3 +58,13 @@ function scoreUsersByContract(contract: Contract, bets: Bet[]) {
|
||||||
|
|
||||||
return userScore
|
return userScore
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function addUserScores(
|
||||||
|
src: { [userId: string]: number },
|
||||||
|
dest: { [userId: string]: number }
|
||||||
|
) {
|
||||||
|
for (const [userId, score] of Object.entries(src)) {
|
||||||
|
if (dest[userId] === undefined) dest[userId] = 0
|
||||||
|
dest[userId] += score
|
||||||
|
}
|
||||||
|
}
|
|
@ -21,7 +21,7 @@ export function parseTags(text: string) {
|
||||||
export function parseWordsAsTags(text: string) {
|
export function parseWordsAsTags(text: string) {
|
||||||
const taggedText = text
|
const taggedText = text
|
||||||
.split(/\s+/)
|
.split(/\s+/)
|
||||||
.map((tag) => `#${tag}`)
|
.map((tag) => (tag.startsWith('#') ? tag : `#${tag}`))
|
||||||
.join(' ')
|
.join(' ')
|
||||||
return parseTags(taggedText)
|
return parseTags(taggedText)
|
||||||
}
|
}
|
||||||
|
|
13
common/yarn.lock
Normal file
13
common/yarn.lock
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||||
|
# yarn lockfile v1
|
||||||
|
|
||||||
|
|
||||||
|
"@types/lodash@4.14.178":
|
||||||
|
version "4.14.178"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.178.tgz#341f6d2247db528d4a13ddbb374bcdc80406f4f8"
|
||||||
|
integrity sha512-0d5Wd09ItQWH1qFbEyQ7oTQ3GZrMfth5JkbN3EvTKLXcHLRDSXeLnlvlOn0wvxVIwK5o2M8JzP/OWz7T3NRsbw==
|
||||||
|
|
||||||
|
lodash@4.17.21:
|
||||||
|
version "4.17.21"
|
||||||
|
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
|
||||||
|
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
|
|
@ -1,5 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "functions",
|
"name": "functions",
|
||||||
|
"version": "1.0.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc",
|
"build": "tsc",
|
||||||
"watch": "tsc -w",
|
"watch": "tsc -w",
|
||||||
|
|
|
@ -59,6 +59,7 @@ export const createFold = functions.runWith({ minInstances: 1 }).https.onCall(
|
||||||
name,
|
name,
|
||||||
about,
|
about,
|
||||||
tags,
|
tags,
|
||||||
|
lowercaseTags: tags.map((tag) => tag.toLowerCase()),
|
||||||
createdTime: Date.now(),
|
createdTime: Date.now(),
|
||||||
contractIds: [],
|
contractIds: [],
|
||||||
excludedContractIds: [],
|
excludedContractIds: [],
|
||||||
|
|
34
functions/src/scripts/lowercase-fold-tags.ts
Normal file
34
functions/src/scripts/lowercase-fold-tags.ts
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
import * as admin from 'firebase-admin'
|
||||||
|
import * as _ from 'lodash'
|
||||||
|
|
||||||
|
import { initAdmin } from './script-init'
|
||||||
|
initAdmin('james')
|
||||||
|
|
||||||
|
import { getValues } from '../utils'
|
||||||
|
import { Fold } from '../../../common/fold'
|
||||||
|
|
||||||
|
async function lowercaseFoldTags() {
|
||||||
|
const firestore = admin.firestore()
|
||||||
|
console.log('Updating fold tags')
|
||||||
|
|
||||||
|
const folds = await getValues<Fold>(firestore.collection('folds'))
|
||||||
|
|
||||||
|
console.log('Loaded', folds.length, 'folds')
|
||||||
|
|
||||||
|
for (const fold of folds) {
|
||||||
|
const foldRef = firestore.doc(`folds/${fold.id}`)
|
||||||
|
|
||||||
|
const { tags } = fold
|
||||||
|
const lowercaseTags = _.uniq(tags.map((tag) => tag.toLowerCase()))
|
||||||
|
|
||||||
|
console.log('Adding lowercase tags', fold.slug, lowercaseTags)
|
||||||
|
|
||||||
|
await foldRef.update({
|
||||||
|
lowercaseTags,
|
||||||
|
} as Partial<Fold>)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (require.main === module) {
|
||||||
|
lowercaseFoldTags().then(() => process.exit())
|
||||||
|
}
|
|
@ -16,6 +16,7 @@ export const updateContractMetrics = functions.pubsub
|
||||||
const contracts = await getValues<Contract>(
|
const contracts = await getValues<Contract>(
|
||||||
firestore.collection('contracts')
|
firestore.collection('contracts')
|
||||||
)
|
)
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
contracts.map(async (contract) => {
|
contracts.map(async (contract) => {
|
||||||
const volume24Hours = await computeVolumeFrom(contract, oneDay)
|
const volume24Hours = await computeVolumeFrom(contract, oneDay)
|
||||||
|
|
|
@ -7,7 +7,6 @@ import { Contract } from '../../common/contract'
|
||||||
import { Bet } from '../../common/bet'
|
import { Bet } from '../../common/bet'
|
||||||
import { User } from '../../common/user'
|
import { User } from '../../common/user'
|
||||||
import { calculatePayout } from '../../common/calculate'
|
import { calculatePayout } from '../../common/calculate'
|
||||||
import { StripeTransaction } from '.'
|
|
||||||
|
|
||||||
const firestore = admin.firestore()
|
const firestore = admin.firestore()
|
||||||
|
|
||||||
|
@ -29,16 +28,9 @@ export const updateUserMetrics = functions.pubsub
|
||||||
user,
|
user,
|
||||||
contractsDict
|
contractsDict
|
||||||
)
|
)
|
||||||
const deposits = await getValues<StripeTransaction>(
|
|
||||||
firestore
|
|
||||||
.collection('stripe-transactions')
|
|
||||||
.where('userId', '==', user.id)
|
|
||||||
)
|
|
||||||
const totalDeposits =
|
|
||||||
1000 + _.sumBy(deposits, (deposit) => deposit.manticDollarQuantity)
|
|
||||||
const totalValue = user.balance + investmentValue
|
const totalValue = user.balance + investmentValue
|
||||||
|
|
||||||
const totalPnL = totalValue - totalDeposits
|
const totalPnL = totalValue - user.totalDeposits
|
||||||
|
|
||||||
const creatorVolume = await computeTotalVolume(user, contractsDict)
|
const creatorVolume = await computeTotalVolume(user, contractsDict)
|
||||||
|
|
||||||
|
|
12
package.json
Normal file
12
package.json
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"name": "mantic",
|
||||||
|
"private": true,
|
||||||
|
"workspaces": [
|
||||||
|
"common",
|
||||||
|
"functions",
|
||||||
|
"web"
|
||||||
|
],
|
||||||
|
"scripts": {},
|
||||||
|
"dependencies": {},
|
||||||
|
"devDependencies": {}
|
||||||
|
}
|
|
@ -60,7 +60,7 @@ export function AmountInput(props: {
|
||||||
inputClassName
|
inputClassName
|
||||||
)}
|
)}
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
type="text"
|
type="number"
|
||||||
placeholder="0"
|
placeholder="0"
|
||||||
maxLength={9}
|
maxLength={9}
|
||||||
value={amount ?? ''}
|
value={amount ?? ''}
|
||||||
|
|
|
@ -4,6 +4,7 @@ export function ConfirmationButton(props: {
|
||||||
id: string
|
id: string
|
||||||
openModelBtn: {
|
openModelBtn: {
|
||||||
label: string
|
label: string
|
||||||
|
icon?: any
|
||||||
className?: string
|
className?: string
|
||||||
}
|
}
|
||||||
cancelBtn?: {
|
cancelBtn?: {
|
||||||
|
@ -25,7 +26,7 @@ export function ConfirmationButton(props: {
|
||||||
htmlFor={id}
|
htmlFor={id}
|
||||||
className={clsx('btn modal-button', openModelBtn.className)}
|
className={clsx('btn modal-button', openModelBtn.className)}
|
||||||
>
|
>
|
||||||
{openModelBtn.label}
|
{openModelBtn.icon} {openModelBtn.label}
|
||||||
</label>
|
</label>
|
||||||
<input type="checkbox" id={id} className="modal-toggle" />
|
<input type="checkbox" id={id} className="modal-toggle" />
|
||||||
|
|
||||||
|
|
|
@ -540,7 +540,8 @@ function FeedBetGroup(props: { activityItem: any }) {
|
||||||
|
|
||||||
const [yesBets, noBets] = _.partition(bets, (bet) => bet.outcome === 'YES')
|
const [yesBets, noBets] = _.partition(bets, (bet) => bet.outcome === 'YES')
|
||||||
|
|
||||||
const createdTime = bets[0].createdTime
|
// Use the time of the last bet for the entire group
|
||||||
|
const createdTime = bets[bets.length - 1].createdTime
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
|
@ -18,16 +18,19 @@ import { ContractFeed } from './contract-feed'
|
||||||
import { TweetButton } from './tweet-button'
|
import { TweetButton } from './tweet-button'
|
||||||
import { Bet } from '../../common/bet'
|
import { Bet } from '../../common/bet'
|
||||||
import { Comment } from '../../common/comment'
|
import { Comment } from '../../common/comment'
|
||||||
import { TagsInput } from './tags-input'
|
import { RevealableTagsInput, TagsInput } from './tags-input'
|
||||||
import BetRow from './bet-row'
|
import BetRow from './bet-row'
|
||||||
|
import { Fold } from '../../common/fold'
|
||||||
|
import { FoldTagList } from './tags-list'
|
||||||
|
|
||||||
export const ContractOverview = (props: {
|
export const ContractOverview = (props: {
|
||||||
contract: Contract
|
contract: Contract
|
||||||
bets: Bet[]
|
bets: Bet[]
|
||||||
comments: Comment[]
|
comments: Comment[]
|
||||||
|
folds: Fold[]
|
||||||
className?: string
|
className?: string
|
||||||
}) => {
|
}) => {
|
||||||
const { contract, bets, comments, className } = props
|
const { contract, bets, comments, folds, className } = props
|
||||||
const { resolution, creatorId, creatorName } = contract
|
const { resolution, creatorId, creatorName } = contract
|
||||||
const { probPercent, truePool } = contractMetrics(contract)
|
const { probPercent, truePool } = contractMetrics(contract)
|
||||||
|
|
||||||
|
@ -85,11 +88,28 @@ export const ContractOverview = (props: {
|
||||||
|
|
||||||
<ContractProbGraph contract={contract} />
|
<ContractProbGraph contract={contract} />
|
||||||
|
|
||||||
<Row className="justify-between mt-6 ml-4 gap-4">
|
<Row className="hidden sm:flex justify-between items-center mt-6 ml-4 gap-4">
|
||||||
<TagsInput contract={contract} />
|
{folds.length === 0 ? (
|
||||||
|
<TagsInput className={clsx('mx-4')} contract={contract} />
|
||||||
|
) : (
|
||||||
|
<FoldTagList folds={folds} />
|
||||||
|
)}
|
||||||
<TweetButton tweetText={tweetText} />
|
<TweetButton tweetText={tweetText} />
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
|
<Col className="sm:hidden mt-6 ml-4 gap-4">
|
||||||
|
<TweetButton className="self-end" tweetText={tweetText} />
|
||||||
|
{folds.length === 0 ? (
|
||||||
|
<TagsInput contract={contract} />
|
||||||
|
) : (
|
||||||
|
<FoldTagList folds={folds} />
|
||||||
|
)}
|
||||||
|
</Col>
|
||||||
|
|
||||||
|
{folds.length > 0 && (
|
||||||
|
<RevealableTagsInput className="mt-4 mx-4" contract={contract} />
|
||||||
|
)}
|
||||||
|
|
||||||
<Spacer h={12} />
|
<Spacer h={12} />
|
||||||
|
|
||||||
{/* Show a delete button for contracts without any trading */}
|
{/* Show a delete button for contracts without any trading */}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
|
import { PlusCircleIcon } from '@heroicons/react/solid'
|
||||||
import { parseWordsAsTags } from '../../common/util/parse'
|
import { parseWordsAsTags } from '../../common/util/parse'
|
||||||
import { createFold } from '../lib/firebase/api-call'
|
import { createFold } from '../lib/firebase/api-call'
|
||||||
import { foldPath } from '../lib/firebase/folds'
|
import { foldPath } from '../lib/firebase/folds'
|
||||||
|
@ -49,7 +50,8 @@ export function CreateFoldButton() {
|
||||||
<ConfirmationButton
|
<ConfirmationButton
|
||||||
id="create-fold"
|
id="create-fold"
|
||||||
openModelBtn={{
|
openModelBtn={{
|
||||||
label: 'Create a fold',
|
label: 'New',
|
||||||
|
icon: <PlusCircleIcon className="w-5 h-5 mr-2" />,
|
||||||
className: clsx(
|
className: clsx(
|
||||||
isSubmitting ? 'loading btn-disabled' : 'btn-primary',
|
isSubmitting ? 'loading btn-disabled' : 'btn-primary',
|
||||||
'btn-sm'
|
'btn-sm'
|
||||||
|
@ -61,11 +63,11 @@ export function CreateFoldButton() {
|
||||||
}}
|
}}
|
||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
>
|
>
|
||||||
<Title className="!mt-0" text="Create a fold" />
|
<Title className="!mt-0" text="Create a community" />
|
||||||
|
|
||||||
<Col className="text-gray-500 gap-1">
|
<Col className="text-gray-500 gap-1">
|
||||||
<div>
|
<div>
|
||||||
Markets are included in a fold if they match one or more tags.
|
Markets are included in a community if they match one or more tags.
|
||||||
</div>
|
</div>
|
||||||
</Col>
|
</Col>
|
||||||
|
|
||||||
|
@ -74,11 +76,11 @@ export function CreateFoldButton() {
|
||||||
<div>
|
<div>
|
||||||
<div className="form-control w-full">
|
<div className="form-control w-full">
|
||||||
<label className="label">
|
<label className="label">
|
||||||
<span className="mb-1">Fold name</span>
|
<span className="mb-1">Community name</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
placeholder="Your fold name"
|
placeholder="Name"
|
||||||
className="input input-bordered resize-none"
|
className="input input-bordered resize-none"
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
value={name}
|
value={name}
|
||||||
|
@ -109,7 +111,7 @@ export function CreateFoldButton() {
|
||||||
<label className="label">
|
<label className="label">
|
||||||
<span className="mb-1">Primary tag</span>
|
<span className="mb-1">Primary tag</span>
|
||||||
</label>
|
</label>
|
||||||
<TagsList noLink tags={[`#${toCamelCase(name)}`]} />
|
<TagsList noLink noLabel tags={[`#${toCamelCase(name)}`]} />
|
||||||
|
|
||||||
<Spacer h={4} />
|
<Spacer h={4} />
|
||||||
|
|
||||||
|
@ -132,6 +134,7 @@ export function CreateFoldButton() {
|
||||||
<TagsList
|
<TagsList
|
||||||
tags={parseWordsAsTags(otherTags).map((tag) => `#${tag}`)}
|
tags={parseWordsAsTags(otherTags).map((tag) => `#${tag}`)}
|
||||||
noLink
|
noLink
|
||||||
|
noLabel
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</ConfirmationButton>
|
</ConfirmationButton>
|
||||||
|
|
|
@ -59,7 +59,7 @@ export function EditFoldButton(props: { fold: Fold; className?: string }) {
|
||||||
<div className="modal-box">
|
<div className="modal-box">
|
||||||
<div className="form-control w-full">
|
<div className="form-control w-full">
|
||||||
<label className="label">
|
<label className="label">
|
||||||
<span className="mb-1">Fold name</span>
|
<span className="mb-1">Community name</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
|
@ -105,7 +105,7 @@ export function EditFoldButton(props: { fold: Fold; className?: string }) {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Spacer h={4} />
|
<Spacer h={4} />
|
||||||
<TagsList tags={tags.map((tag) => `#${tag}`)} noLink />
|
<TagsList tags={tags.map((tag) => `#${tag}`)} noLink noLabel />
|
||||||
<Spacer h={4} />
|
<Spacer h={4} />
|
||||||
|
|
||||||
<div className="modal-action">
|
<div className="modal-action">
|
||||||
|
|
|
@ -81,7 +81,7 @@ export default function FeedCreate(props: {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={clsx('w-full bg-white p-4 border-b-2 mt-2', className)}
|
className={clsx('w-full bg-white p-4 shadow-md mt-2', className)}
|
||||||
onClick={() => !question && inputRef.current?.focus()}
|
onClick={() => !question && inputRef.current?.focus()}
|
||||||
>
|
>
|
||||||
<div className="relative flex items-start space-x-3">
|
<div className="relative flex items-start space-x-3">
|
||||||
|
|
17
web/components/fold-tag.tsx
Normal file
17
web/components/fold-tag.tsx
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import clsx from 'clsx'
|
||||||
|
import { Fold } from '../../common/fold'
|
||||||
|
|
||||||
|
export function FoldTag(props: { fold: Fold }) {
|
||||||
|
const { fold } = props
|
||||||
|
const { name } = fold
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
'bg-white hover:bg-gray-100 px-4 py-2 rounded-full shadow-md',
|
||||||
|
'cursor-pointer'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span className="text-gray-500">{name}</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
|
@ -16,7 +16,7 @@ export function Leaderboard(props: {
|
||||||
const { title, users, columns, className } = props
|
const { title, users, columns, className } = props
|
||||||
return (
|
return (
|
||||||
<div className={clsx('w-full px-1', className)}>
|
<div className={clsx('w-full px-1', className)}>
|
||||||
<Title text={title} />
|
<Title text={title} className="!mt-0" />
|
||||||
{users.length === 0 ? (
|
{users.length === 0 ? (
|
||||||
<div className="text-gray-500 ml-2">None yet</div>
|
<div className="text-gray-500 ml-2">None yet</div>
|
||||||
) : (
|
) : (
|
||||||
|
|
|
@ -6,6 +6,12 @@ import { Row } from './layout/row'
|
||||||
import { firebaseLogin, User } from '../lib/firebase/users'
|
import { firebaseLogin, User } from '../lib/firebase/users'
|
||||||
import { ManifoldLogo } from './manifold-logo'
|
import { ManifoldLogo } from './manifold-logo'
|
||||||
import { ProfileMenu } from './profile-menu'
|
import { ProfileMenu } from './profile-menu'
|
||||||
|
import {
|
||||||
|
CollectionIcon,
|
||||||
|
HomeIcon,
|
||||||
|
SearchIcon,
|
||||||
|
UserGroupIcon,
|
||||||
|
} from '@heroicons/react/outline'
|
||||||
|
|
||||||
export function NavBar(props: {
|
export function NavBar(props: {
|
||||||
darkBackground?: boolean
|
darkBackground?: boolean
|
||||||
|
@ -22,25 +28,76 @@ export function NavBar(props: {
|
||||||
const themeClasses = clsx(darkBackground && 'text-white', hoverClasses)
|
const themeClasses = clsx(darkBackground && 'text-white', hoverClasses)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav className={clsx('w-full p-4 mb-4', className)} aria-label="Global">
|
<>
|
||||||
<Row
|
<nav className={clsx('w-full p-4 mb-4', className)} aria-label="Global">
|
||||||
className={clsx(
|
<Row
|
||||||
'justify-between items-center mx-auto sm:px-4',
|
className={clsx(
|
||||||
wide ? 'max-w-6xl' : 'max-w-4xl'
|
'justify-between items-center mx-auto sm:px-4',
|
||||||
)}
|
wide ? 'max-w-6xl' : 'max-w-4xl'
|
||||||
>
|
|
||||||
<ManifoldLogo className="my-1" darkBackground={darkBackground} />
|
|
||||||
|
|
||||||
<Row className="items-center gap-6 sm:gap-8 ml-6">
|
|
||||||
{(user || user === null || assertUser) && (
|
|
||||||
<NavOptions
|
|
||||||
user={user}
|
|
||||||
assertUser={assertUser}
|
|
||||||
themeClasses={themeClasses}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
|
>
|
||||||
|
<ManifoldLogo className="my-1" darkBackground={darkBackground} />
|
||||||
|
|
||||||
|
<Row className="items-center gap-6 sm:gap-8 ml-6">
|
||||||
|
{(user || user === null || assertUser) && (
|
||||||
|
<NavOptions
|
||||||
|
user={user}
|
||||||
|
assertUser={assertUser}
|
||||||
|
themeClasses={themeClasses}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Row>
|
||||||
</Row>
|
</Row>
|
||||||
</Row>
|
</nav>
|
||||||
|
{user && <BottomNavBar user={user} />}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// From https://codepen.io/chris__sev/pen/QWGvYbL
|
||||||
|
function BottomNavBar(props: { user: User }) {
|
||||||
|
const { user } = props
|
||||||
|
return (
|
||||||
|
<nav className="md:hidden fixed bottom-0 inset-x-0 bg-white z-20 flex justify-between text-xs text-gray-700 border-t-2">
|
||||||
|
<Link href="/home">
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
className="w-full block py-2 px-3 text-center hover:bg-indigo-200 hover:text-indigo-700 transition duration-300"
|
||||||
|
>
|
||||||
|
<HomeIcon className="h-6 w-6 my-1 mx-auto" aria-hidden="true" />
|
||||||
|
{/* Home */}
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<Link href="/markets">
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
className="w-full block py-2 px-3 text-center hover:bg-indigo-200 hover:text-indigo-700"
|
||||||
|
>
|
||||||
|
<SearchIcon className="h-6 w-6 my-1 mx-auto" aria-hidden="true" />
|
||||||
|
{/* Explore */}
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<Link href="/folds">
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
className="w-full block py-2 px-3 text-center hover:bg-indigo-200 hover:text-indigo-700"
|
||||||
|
>
|
||||||
|
<UserGroupIcon className="h-6 w-6 my-1 mx-auto" aria-hidden="true" />
|
||||||
|
{/* Folds */}
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<Link href="/trades">
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
className="w-full block py-2 px-3 text-center hover:bg-indigo-200 hover:text-indigo-700"
|
||||||
|
>
|
||||||
|
<CollectionIcon className="h-6 w-6 my-1 mx-auto" aria-hidden="true" />
|
||||||
|
{/* Your Trades */}
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
</nav>
|
</nav>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -77,7 +134,7 @@ function NavOptions(props: {
|
||||||
themeClasses
|
themeClasses
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
Folds
|
Communities
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
|
|
|
@ -15,7 +15,7 @@ export function Page(props: {
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'w-full mx-auto',
|
'w-full mx-auto pb-16',
|
||||||
wide ? 'max-w-6xl' : 'max-w-4xl',
|
wide ? 'max-w-6xl' : 'max-w-4xl',
|
||||||
margin && 'px-4'
|
margin && 'px-4'
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
|
import clsx from 'clsx'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { parseWordsAsTags } from '../../common/util/parse'
|
import { parseWordsAsTags } from '../../common/util/parse'
|
||||||
import { Contract, updateContract } from '../lib/firebase/contracts'
|
import { Contract, updateContract } from '../lib/firebase/contracts'
|
||||||
|
import { Col } from './layout/col'
|
||||||
import { Row } from './layout/row'
|
import { Row } from './layout/row'
|
||||||
import { TagsList } from './tags-list'
|
import { TagsList } from './tags-list'
|
||||||
|
|
||||||
export function TagsInput(props: { contract: Contract }) {
|
export function TagsInput(props: { contract: Contract; className?: string }) {
|
||||||
const { contract } = props
|
const { contract, className } = props
|
||||||
const { tags } = contract
|
const { tags } = contract
|
||||||
|
|
||||||
const [tagText, setTagText] = useState('')
|
const [tagText, setTagText] = useState('')
|
||||||
|
@ -24,7 +26,7 @@ export function TagsInput(props: { contract: Contract }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Row className="flex-wrap gap-4">
|
<Col className={clsx('gap-4', className)}>
|
||||||
<TagsList tags={newTags.map((tag) => `#${tag}`)} />
|
<TagsList tags={newTags.map((tag) => `#${tag}`)} />
|
||||||
|
|
||||||
<Row className="items-center gap-4">
|
<Row className="items-center gap-4">
|
||||||
|
@ -40,6 +42,28 @@ export function TagsInput(props: { contract: Contract }) {
|
||||||
Save tags
|
Save tags
|
||||||
</button>
|
</button>
|
||||||
</Row>
|
</Row>
|
||||||
</Row>
|
</Col>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function RevealableTagsInput(props: {
|
||||||
|
contract: Contract
|
||||||
|
className?: string
|
||||||
|
}) {
|
||||||
|
const { contract, className } = props
|
||||||
|
const [hidden, setHidden] = useState(true)
|
||||||
|
|
||||||
|
if (hidden)
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
'text-gray-500 cursor-pointer hover:underline hover:decoration-indigo-400 hover:decoration-2',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
onClick={() => setHidden((hidden) => !hidden)}
|
||||||
|
>
|
||||||
|
Show tags
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
return <TagsInput className={clsx('pt-2', className)} contract={contract} />
|
||||||
|
}
|
||||||
|
|
|
@ -1,18 +1,18 @@
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
import { Row } from './layout/row'
|
import { Row } from './layout/row'
|
||||||
import { Linkify } from './linkify'
|
|
||||||
import { SiteLink } from './site-link'
|
import { SiteLink } from './site-link'
|
||||||
|
import { Fold } from '../../common/fold'
|
||||||
|
|
||||||
export function Hashtag(props: { tag: string; noLink?: boolean }) {
|
export function Hashtag(props: { tag: string; noLink?: boolean }) {
|
||||||
const { tag, noLink } = props
|
const { tag, noLink } = props
|
||||||
const body = (
|
const body = (
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'bg-white hover:bg-gray-100 px-4 py-2 rounded-full shadow-md',
|
'bg-gray-100 border-2 px-3 py-1 rounded-full shadow-md',
|
||||||
!noLink && 'cursor-pointer'
|
!noLink && 'cursor-pointer'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<span className="text-gray-500">{tag}</span>
|
<span className="text-gray-600 text-sm">{tag}</span>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -28,10 +28,12 @@ export function TagsList(props: {
|
||||||
tags: string[]
|
tags: string[]
|
||||||
className?: string
|
className?: string
|
||||||
noLink?: boolean
|
noLink?: boolean
|
||||||
|
noLabel?: boolean
|
||||||
}) {
|
}) {
|
||||||
const { tags, className, noLink } = props
|
const { tags, className, noLink, noLabel } = props
|
||||||
return (
|
return (
|
||||||
<Row className={clsx('flex-wrap gap-2', className)}>
|
<Row className={clsx('items-center flex-wrap gap-2', className)}>
|
||||||
|
{!noLabel && <div className="text-gray-500 mr-1">Tags</div>}
|
||||||
{tags.map((tag) => (
|
{tags.map((tag) => (
|
||||||
<Hashtag key={tag} tag={tag} noLink={noLink} />
|
<Hashtag key={tag} tag={tag} noLink={noLink} />
|
||||||
))}
|
))}
|
||||||
|
@ -39,15 +41,35 @@ export function TagsList(props: {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CompactTagsList(props: { tags: string[] }) {
|
export function FoldTag(props: { fold: Fold }) {
|
||||||
const { tags } = props
|
const { fold } = props
|
||||||
|
const { name } = fold
|
||||||
return (
|
return (
|
||||||
<Row className="gap-2 flex-wrap text-sm text-gray-500">
|
<SiteLink href={`/fold/${fold.slug}`} className="flex items-center">
|
||||||
{tags.map((tag) => (
|
<div
|
||||||
<div key={tag} className="bg-gray-100 px-1">
|
className={clsx(
|
||||||
<Linkify text={tag} gray />
|
'bg-white border-2 px-4 py-1 rounded-full shadow-md',
|
||||||
</div>
|
'cursor-pointer'
|
||||||
))}
|
)}
|
||||||
|
>
|
||||||
|
<span className="text-gray-500 text-sm">{name}</span>
|
||||||
|
</div>
|
||||||
|
</SiteLink>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FoldTagList(props: { folds: Fold[]; className?: string }) {
|
||||||
|
const { folds, className } = props
|
||||||
|
return (
|
||||||
|
<Row className={clsx('flex-wrap gap-2 items-center', className)}>
|
||||||
|
{folds.length > 0 && (
|
||||||
|
<>
|
||||||
|
<div className="text-gray-500 mr-1">Communities</div>
|
||||||
|
{folds.map((fold) => (
|
||||||
|
<FoldTag key={fold.id} fold={fold} />
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</Row>
|
</Row>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import {
|
||||||
query,
|
query,
|
||||||
onSnapshot,
|
onSnapshot,
|
||||||
where,
|
where,
|
||||||
|
orderBy,
|
||||||
} from 'firebase/firestore'
|
} from 'firebase/firestore'
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
|
|
||||||
|
@ -23,6 +24,29 @@ export async function listAllBets(contractId: string) {
|
||||||
return bets
|
return bets
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const DAY_IN_MS = 24 * 60 * 60 * 1000
|
||||||
|
|
||||||
|
// Define "recent" as "<24 hours ago" for now
|
||||||
|
const recentBetsQuery = query(
|
||||||
|
collectionGroup(db, 'bets'),
|
||||||
|
where('createdTime', '>', Date.now() - DAY_IN_MS),
|
||||||
|
orderBy('createdTime', 'desc')
|
||||||
|
)
|
||||||
|
|
||||||
|
export async function getRecentBets() {
|
||||||
|
return getValues<Bet>(recentBetsQuery)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getRecentContractBets(contractId: string) {
|
||||||
|
const q = query(
|
||||||
|
getBetsCollection(contractId),
|
||||||
|
where('createdTime', '>', Date.now() - DAY_IN_MS),
|
||||||
|
orderBy('createdTime', 'desc')
|
||||||
|
)
|
||||||
|
|
||||||
|
return getValues<Bet>(q)
|
||||||
|
}
|
||||||
|
|
||||||
export function listenForBets(
|
export function listenForBets(
|
||||||
contractId: string,
|
contractId: string,
|
||||||
setBets: (bets: Bet[]) => void
|
setBets: (bets: Bet[]) => void
|
||||||
|
|
|
@ -7,6 +7,7 @@ import {
|
||||||
updateDoc,
|
updateDoc,
|
||||||
where,
|
where,
|
||||||
} from 'firebase/firestore'
|
} from 'firebase/firestore'
|
||||||
|
import _ from 'lodash'
|
||||||
import { Fold } from '../../../common/fold'
|
import { Fold } from '../../../common/fold'
|
||||||
import { Contract, contractCollection } from './contracts'
|
import { Contract, contractCollection } from './contracts'
|
||||||
import { db } from './init'
|
import { db } from './init'
|
||||||
|
@ -131,3 +132,18 @@ export function listenForFollow(
|
||||||
setFollow(!!value)
|
setFollow(!!value)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getFoldsByTags(tags: string[]) {
|
||||||
|
if (tags.length === 0) return []
|
||||||
|
|
||||||
|
const lowercaseTags = tags.map((tag) => tag.toLowerCase())
|
||||||
|
const folds = await getValues<Fold>(
|
||||||
|
// TODO: split into multiple queries if tags.length > 10.
|
||||||
|
query(
|
||||||
|
foldCollection,
|
||||||
|
where('lowercaseTags', 'array-contains-any', lowercaseTags)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return _.sortBy(folds, (fold) => -1 * fold.followCount)
|
||||||
|
}
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
import { getFirestore } from '@firebase/firestore'
|
import { getFirestore } from '@firebase/firestore'
|
||||||
import { initializeApp, getApps, getApp } from 'firebase/app'
|
import { initializeApp, getApps, getApp } from 'firebase/app'
|
||||||
|
|
||||||
// TODO: Reenable this when we have a way to set the Firebase db in dev
|
export const isProd = process.env.NEXT_PUBLIC_FIREBASE_ENV !== 'DEV'
|
||||||
// export const isProd = process.env.NODE_ENV === 'production'
|
|
||||||
export const isProd = true
|
|
||||||
|
|
||||||
const firebaseConfig = isProd
|
const firebaseConfig = isProd
|
||||||
? {
|
? {
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
{
|
{
|
||||||
"name": "mantic",
|
"name": "web",
|
||||||
|
"version": "1.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "concurrently -n NEXT,TS -c magenta,cyan \"next dev -p 3000\" \"yarn ts --watch\"",
|
"dev": "concurrently -n NEXT,TS -c magenta,cyan \"next dev -p 3000\" \"yarn ts --watch\"",
|
||||||
|
"devdev": "NEXT_PUBLIC_FIREBASE_ENV=DEV concurrently -n NEXT,TS -c magenta,cyan \"FIREBASE_ENV=DEV next dev -p 3000\" \"FIREBASE_ENV=DEV yarn ts --watch\"",
|
||||||
"ts": "tsc --noEmit --incremental --preserveWatchOutput --pretty",
|
"ts": "tsc --noEmit --incremental --preserveWatchOutput --pretty",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
|
|
|
@ -23,6 +23,8 @@ import { contractTextDetails } from '../../components/contract-card'
|
||||||
import { Bet, listAllBets } from '../../lib/firebase/bets'
|
import { Bet, listAllBets } from '../../lib/firebase/bets'
|
||||||
import { Comment, listAllComments } from '../../lib/firebase/comments'
|
import { Comment, listAllComments } from '../../lib/firebase/comments'
|
||||||
import Custom404 from '../404'
|
import Custom404 from '../404'
|
||||||
|
import { getFoldsByTags } from '../../lib/firebase/folds'
|
||||||
|
import { Fold } from '../../../common/fold'
|
||||||
|
|
||||||
export async function getStaticProps(props: {
|
export async function getStaticProps(props: {
|
||||||
params: { username: string; contractSlug: string }
|
params: { username: string; contractSlug: string }
|
||||||
|
@ -31,18 +33,23 @@ export async function getStaticProps(props: {
|
||||||
const contract = (await getContractFromSlug(contractSlug)) || null
|
const contract = (await getContractFromSlug(contractSlug)) || null
|
||||||
const contractId = contract?.id
|
const contractId = contract?.id
|
||||||
|
|
||||||
|
const foldsPromise = getFoldsByTags(contract?.tags ?? [])
|
||||||
|
|
||||||
const [bets, comments] = await Promise.all([
|
const [bets, comments] = await Promise.all([
|
||||||
contractId ? listAllBets(contractId) : null,
|
contractId ? listAllBets(contractId) : [],
|
||||||
contractId ? listAllComments(contractId) : null,
|
contractId ? listAllComments(contractId) : [],
|
||||||
])
|
])
|
||||||
|
|
||||||
|
const folds = await foldsPromise
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
|
contract,
|
||||||
username,
|
username,
|
||||||
slug: contractSlug,
|
slug: contractSlug,
|
||||||
contract,
|
|
||||||
bets,
|
bets,
|
||||||
comments,
|
comments,
|
||||||
|
folds,
|
||||||
},
|
},
|
||||||
|
|
||||||
revalidate: 60, // regenerate after a minute
|
revalidate: 60, // regenerate after a minute
|
||||||
|
@ -55,15 +62,16 @@ export async function getStaticPaths() {
|
||||||
|
|
||||||
export default function ContractPage(props: {
|
export default function ContractPage(props: {
|
||||||
contract: Contract | null
|
contract: Contract | null
|
||||||
bets: Bet[] | null
|
|
||||||
comments: Comment[] | null
|
|
||||||
slug: string
|
|
||||||
username: string
|
username: string
|
||||||
|
bets: Bet[]
|
||||||
|
comments: Comment[]
|
||||||
|
slug: string
|
||||||
|
folds: Fold[]
|
||||||
}) {
|
}) {
|
||||||
const user = useUser()
|
const user = useUser()
|
||||||
|
|
||||||
const contract = useContractWithPreload(props.slug, props.contract)
|
const contract = useContractWithPreload(props.slug, props.contract)
|
||||||
const { bets, comments } = props
|
const { bets, comments, folds } = props
|
||||||
|
|
||||||
if (!contract) {
|
if (!contract) {
|
||||||
return <Custom404 />
|
return <Custom404 />
|
||||||
|
@ -103,6 +111,7 @@ export default function ContractPage(props: {
|
||||||
contract={contract}
|
contract={contract}
|
||||||
bets={bets ?? []}
|
bets={bets ?? []}
|
||||||
comments={comments ?? []}
|
comments={comments ?? []}
|
||||||
|
folds={folds}
|
||||||
/>
|
/>
|
||||||
<BetsSection contract={contract} user={user ?? null} />
|
<BetsSection contract={contract} user={user ?? null} />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { cloneElement } from 'react'
|
import { cloneElement } from 'react'
|
||||||
import { Page } from '../components/page'
|
import { Page } from '../components/page'
|
||||||
import { SEO } from '../components/SEO'
|
import { SEO } from '../components/SEO'
|
||||||
|
import { useContracts } from '../hooks/use-contracts'
|
||||||
import styles from './about.module.css'
|
import styles from './about.module.css'
|
||||||
|
|
||||||
export default function About() {
|
export default function About() {
|
||||||
|
@ -244,7 +245,18 @@ function Contents() {
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
Office hours:{' '}
|
Office hours:{' '}
|
||||||
<a href="https://calendly.com/austinchen/manifold">Calendly</a>
|
<ul>
|
||||||
|
<li>
|
||||||
|
<a href="https://calendly.com/austinchen/manifold">
|
||||||
|
Calendly — Austin
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="https://calendly.com/jamesgrugett/manifold">
|
||||||
|
Calendly — James
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
Chat:{' '}
|
Chat:{' '}
|
||||||
|
|
|
@ -7,6 +7,7 @@ import { Col } from '../components/layout/col'
|
||||||
import { Bet } from '../../common/bet'
|
import { Bet } from '../../common/bet'
|
||||||
|
|
||||||
const MAX_ACTIVE_CONTRACTS = 75
|
const MAX_ACTIVE_CONTRACTS = 75
|
||||||
|
const MAX_HOT_MARKETS = 10
|
||||||
|
|
||||||
// This does NOT include comment times, since those aren't part of the contract atm.
|
// This does NOT include comment times, since those aren't part of the contract atm.
|
||||||
// TODO: Maybe store last activity time directly in the contract?
|
// TODO: Maybe store last activity time directly in the contract?
|
||||||
|
@ -23,9 +24,11 @@ function lastActivityTime(contract: Contract) {
|
||||||
// - Comment on a market
|
// - Comment on a market
|
||||||
// - New market created
|
// - New market created
|
||||||
// - Market resolved
|
// - Market resolved
|
||||||
|
// - Markets with most betting in last 24 hours
|
||||||
export function findActiveContracts(
|
export function findActiveContracts(
|
||||||
allContracts: Contract[],
|
allContracts: Contract[],
|
||||||
recentComments: Comment[],
|
recentComments: Comment[],
|
||||||
|
recentBets: Bet[],
|
||||||
daysAgo = 3
|
daysAgo = 3
|
||||||
) {
|
) {
|
||||||
const idToActivityTime = new Map<string, number>()
|
const idToActivityTime = new Map<string, number>()
|
||||||
|
@ -56,6 +59,26 @@ export function findActiveContracts(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add recent top-trading contracts, ordered by last bet.
|
||||||
|
const contractBets = _.groupBy(recentBets, (bet) => bet.contractId)
|
||||||
|
const contractTotalBets = _.mapValues(contractBets, (bets) =>
|
||||||
|
_.sumBy(bets, (bet) => bet.amount)
|
||||||
|
)
|
||||||
|
const topTradedContracts = _.sortBy(
|
||||||
|
_.toPairs(contractTotalBets),
|
||||||
|
([_, total]) => -1 * total
|
||||||
|
)
|
||||||
|
.map(([id]) => contractsById.get(id) as Contract)
|
||||||
|
.slice(0, MAX_HOT_MARKETS)
|
||||||
|
|
||||||
|
for (const contract of topTradedContracts) {
|
||||||
|
const bet = recentBets.find((bet) => bet.contractId === contract.id)
|
||||||
|
if (bet) {
|
||||||
|
contracts.push(contract)
|
||||||
|
record(contract.id, bet.createdTime)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
contracts = _.uniqBy(contracts, (c) => c.id)
|
contracts = _.uniqBy(contracts, (c) => c.id)
|
||||||
contracts = contracts.filter((contract) => contract.visibility === 'public')
|
contracts = contracts.filter((contract) => contract.visibility === 'public')
|
||||||
contracts = _.sortBy(contracts, (c) => -(idToActivityTime.get(c.id) ?? 0))
|
contracts = _.sortBy(contracts, (c) => -(idToActivityTime.get(c.id) ?? 0))
|
||||||
|
|
|
@ -5,7 +5,11 @@ import { Fold } from '../../../../common/fold'
|
||||||
import { Comment } from '../../../../common/comment'
|
import { Comment } from '../../../../common/comment'
|
||||||
import { Page } from '../../../components/page'
|
import { Page } from '../../../components/page'
|
||||||
import { Title } from '../../../components/title'
|
import { Title } from '../../../components/title'
|
||||||
import { Bet, listAllBets } from '../../../lib/firebase/bets'
|
import {
|
||||||
|
Bet,
|
||||||
|
getRecentContractBets,
|
||||||
|
listAllBets,
|
||||||
|
} from '../../../lib/firebase/bets'
|
||||||
import { listAllComments } from '../../../lib/firebase/comments'
|
import { listAllComments } from '../../../lib/firebase/comments'
|
||||||
import { Contract } from '../../../lib/firebase/contracts'
|
import { Contract } from '../../../lib/firebase/contracts'
|
||||||
import {
|
import {
|
||||||
|
@ -26,7 +30,7 @@ import { SearchableGrid } from '../../../components/contracts-list'
|
||||||
import { useQueryAndSortParams } from '../../../hooks/use-sort-and-query-params'
|
import { useQueryAndSortParams } from '../../../hooks/use-sort-and-query-params'
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
import { scoreCreators, scoreTraders } from '../../../lib/firebase/scoring'
|
import { scoreCreators, scoreTraders } from '../../../../common/scoring'
|
||||||
import { Leaderboard } from '../../../components/leaderboard'
|
import { Leaderboard } from '../../../components/leaderboard'
|
||||||
import { formatMoney, toCamelCase } from '../../../../common/util/format'
|
import { formatMoney, toCamelCase } from '../../../../common/util/format'
|
||||||
import { EditFoldButton } from '../../../components/edit-fold-button'
|
import { EditFoldButton } from '../../../components/edit-fold-button'
|
||||||
|
@ -43,13 +47,26 @@ export async function getStaticProps(props: { params: { slugs: string[] } }) {
|
||||||
const curatorPromise = fold ? getUser(fold.curatorId) : null
|
const curatorPromise = fold ? getUser(fold.curatorId) : null
|
||||||
|
|
||||||
const contracts = fold ? await getFoldContracts(fold).catch((_) => []) : []
|
const contracts = fold ? await getFoldContracts(fold).catch((_) => []) : []
|
||||||
const contractComments = await Promise.all(
|
|
||||||
contracts.map((contract) => listAllComments(contract.id).catch((_) => []))
|
const betsPromise = Promise.all(
|
||||||
|
contracts.map((contract) => listAllBets(contract.id))
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const [contractComments, contractRecentBets] = await Promise.all([
|
||||||
|
Promise.all(
|
||||||
|
contracts.map((contract) => listAllComments(contract.id).catch((_) => []))
|
||||||
|
),
|
||||||
|
Promise.all(
|
||||||
|
contracts.map((contract) =>
|
||||||
|
getRecentContractBets(contract.id).catch((_) => [])
|
||||||
|
)
|
||||||
|
),
|
||||||
|
])
|
||||||
|
|
||||||
let activeContracts = findActiveContracts(
|
let activeContracts = findActiveContracts(
|
||||||
contracts,
|
contracts,
|
||||||
_.flatten(contractComments),
|
_.flatten(contractComments),
|
||||||
|
_.flatten(contractRecentBets),
|
||||||
365
|
365
|
||||||
)
|
)
|
||||||
const [resolved, unresolved] = _.partition(
|
const [resolved, unresolved] = _.partition(
|
||||||
|
@ -66,17 +83,16 @@ export async function getStaticProps(props: { params: { slugs: string[] } }) {
|
||||||
contractComments[contracts.findIndex((c) => c.id === contract.id)]
|
contractComments[contracts.findIndex((c) => c.id === contract.id)]
|
||||||
)
|
)
|
||||||
|
|
||||||
const curator = await curatorPromise
|
const bets = await betsPromise
|
||||||
|
|
||||||
const bets = await Promise.all(
|
|
||||||
contracts.map((contract) => listAllBets(contract.id))
|
|
||||||
)
|
|
||||||
|
|
||||||
const creatorScores = scoreCreators(contracts, bets)
|
const creatorScores = scoreCreators(contracts, bets)
|
||||||
const [topCreators, topCreatorScores] = await toUserScores(creatorScores)
|
|
||||||
|
|
||||||
const traderScores = scoreTraders(contracts, bets)
|
const traderScores = scoreTraders(contracts, bets)
|
||||||
const [topTraders, topTraderScores] = await toUserScores(traderScores)
|
const [topCreators, topTraders] = await Promise.all([
|
||||||
|
toTopUsers(creatorScores),
|
||||||
|
toTopUsers(traderScores),
|
||||||
|
])
|
||||||
|
|
||||||
|
const curator = await curatorPromise
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
|
@ -86,17 +102,17 @@ export async function getStaticProps(props: { params: { slugs: string[] } }) {
|
||||||
activeContracts,
|
activeContracts,
|
||||||
activeContractBets,
|
activeContractBets,
|
||||||
activeContractComments,
|
activeContractComments,
|
||||||
|
traderScores,
|
||||||
topTraders,
|
topTraders,
|
||||||
topTraderScores,
|
creatorScores,
|
||||||
topCreators,
|
topCreators,
|
||||||
topCreatorScores,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
revalidate: 60, // regenerate after a minute
|
revalidate: 60, // regenerate after a minute
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function toUserScores(userScores: { [userId: string]: number }) {
|
async function toTopUsers(userScores: { [userId: string]: number }) {
|
||||||
const topUserPairs = _.take(
|
const topUserPairs = _.take(
|
||||||
_.sortBy(Object.entries(userScores), ([_, score]) => -1 * score),
|
_.sortBy(Object.entries(userScores), ([_, score]) => -1 * score),
|
||||||
10
|
10
|
||||||
|
@ -105,14 +121,7 @@ async function toUserScores(userScores: { [userId: string]: number }) {
|
||||||
const topUsers = await Promise.all(
|
const topUsers = await Promise.all(
|
||||||
topUserPairs.map(([userId]) => getUser(userId))
|
topUserPairs.map(([userId]) => getUser(userId))
|
||||||
)
|
)
|
||||||
const existingPairs = topUserPairs.filter(([id, _]) =>
|
return topUsers.filter((user) => user)
|
||||||
topUsers.find((user) => user?.id === id)
|
|
||||||
)
|
|
||||||
const topExistingUsers = existingPairs.map(
|
|
||||||
([id]) => topUsers.find((user) => user?.id === id) as User
|
|
||||||
)
|
|
||||||
const topUserScores = existingPairs.map(([_, score]) => score)
|
|
||||||
return [topExistingUsers, topUserScores] as const
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getStaticPaths() {
|
export async function getStaticPaths() {
|
||||||
|
@ -127,19 +136,19 @@ export default function FoldPage(props: {
|
||||||
activeContracts: Contract[]
|
activeContracts: Contract[]
|
||||||
activeContractBets: Bet[][]
|
activeContractBets: Bet[][]
|
||||||
activeContractComments: Comment[][]
|
activeContractComments: Comment[][]
|
||||||
|
traderScores: { [userId: string]: number }
|
||||||
topTraders: User[]
|
topTraders: User[]
|
||||||
topTraderScores: number[]
|
creatorScores: { [userId: string]: number }
|
||||||
topCreators: User[]
|
topCreators: User[]
|
||||||
topCreatorScores: number[]
|
|
||||||
}) {
|
}) {
|
||||||
const {
|
const {
|
||||||
curator,
|
curator,
|
||||||
activeContractBets,
|
activeContractBets,
|
||||||
activeContractComments,
|
activeContractComments,
|
||||||
|
traderScores,
|
||||||
topTraders,
|
topTraders,
|
||||||
topTraderScores,
|
creatorScores,
|
||||||
topCreators,
|
topCreators,
|
||||||
topCreatorScores,
|
|
||||||
} = props
|
} = props
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
@ -239,9 +248,9 @@ export default function FoldPage(props: {
|
||||||
{(page === 'activity' || page === 'markets') && (
|
{(page === 'activity' || page === 'markets') && (
|
||||||
<Row className={clsx(page === 'activity' ? 'gap-16' : 'gap-8')}>
|
<Row className={clsx(page === 'activity' ? 'gap-16' : 'gap-8')}>
|
||||||
<Col className="flex-1">
|
<Col className="flex-1">
|
||||||
{user !== null && (
|
{user !== null && !fold.disallowMarketCreation && (
|
||||||
<FeedCreate
|
<FeedCreate
|
||||||
className={clsx(page !== 'activity' && 'hidden')}
|
className={clsx('border-b-2', page !== 'activity' && 'hidden')}
|
||||||
user={user}
|
user={user}
|
||||||
tag={toCamelCase(fold.name)}
|
tag={toCamelCase(fold.name)}
|
||||||
placeholder={`Type your question about ${fold.name}`}
|
placeholder={`Type your question about ${fold.name}`}
|
||||||
|
@ -271,25 +280,28 @@ export default function FoldPage(props: {
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Col>
|
</Col>
|
||||||
<Col className="hidden md:flex max-w-xs w-full gap-10">
|
<Col className="hidden md:flex max-w-xs w-full gap-12">
|
||||||
<FoldOverview fold={fold} curator={curator} />
|
<FoldOverview fold={fold} curator={curator} />
|
||||||
<FoldLeaderboards
|
<FoldLeaderboards
|
||||||
|
traderScores={traderScores}
|
||||||
|
creatorScores={creatorScores}
|
||||||
topTraders={topTraders}
|
topTraders={topTraders}
|
||||||
topTraderScores={topTraderScores}
|
|
||||||
topCreators={topCreators}
|
topCreators={topCreators}
|
||||||
topCreatorScores={topCreatorScores}
|
user={user}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{page === 'leaderboards' && (
|
{page === 'leaderboards' && (
|
||||||
<Col className="gap-8 lg:flex-row">
|
<Col className="gap-8 lg:flex-row px-4">
|
||||||
<FoldLeaderboards
|
<FoldLeaderboards
|
||||||
|
traderScores={traderScores}
|
||||||
|
creatorScores={creatorScores}
|
||||||
topTraders={topTraders}
|
topTraders={topTraders}
|
||||||
topTraderScores={topTraderScores}
|
|
||||||
topCreators={topCreators}
|
topCreators={topCreators}
|
||||||
topCreatorScores={topCreatorScores}
|
user={user}
|
||||||
|
yourPerformanceClassName="lg:hidden"
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
)}
|
)}
|
||||||
|
@ -323,23 +335,67 @@ function FoldOverview(props: { fold: Fold; curator: User }) {
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Spacer h={2} />
|
<div className="divider" />
|
||||||
|
|
||||||
<TagsList tags={tags.map((tag) => `#${tag}`)} />
|
<div className="text-gray-500 mb-2">
|
||||||
|
Includes markets matching any of these tags:
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<TagsList tags={tags.map((tag) => `#${tag}`)} noLabel />
|
||||||
</Col>
|
</Col>
|
||||||
</Col>
|
</Col>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function FoldLeaderboards(props: {
|
function FoldLeaderboards(props: {
|
||||||
|
traderScores: { [userId: string]: number }
|
||||||
|
creatorScores: { [userId: string]: number }
|
||||||
topTraders: User[]
|
topTraders: User[]
|
||||||
topTraderScores: number[]
|
|
||||||
topCreators: User[]
|
topCreators: User[]
|
||||||
topCreatorScores: number[]
|
user: User | null | undefined
|
||||||
|
yourPerformanceClassName?: string
|
||||||
}) {
|
}) {
|
||||||
const { topTraders, topTraderScores, topCreators, topCreatorScores } = props
|
const {
|
||||||
|
traderScores,
|
||||||
|
creatorScores,
|
||||||
|
topTraders,
|
||||||
|
topCreators,
|
||||||
|
user,
|
||||||
|
yourPerformanceClassName,
|
||||||
|
} = props
|
||||||
|
|
||||||
|
const yourTraderScore = user ? traderScores[user.id] : undefined
|
||||||
|
const yourCreatorScore = user ? creatorScores[user.id] : undefined
|
||||||
|
|
||||||
|
const topTraderScores = topTraders.map((user) => traderScores[user.id])
|
||||||
|
const topCreatorScores = topCreators.map((user) => creatorScores[user.id])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
{user && (
|
||||||
|
<Col className={yourPerformanceClassName}>
|
||||||
|
<div className="bg-indigo-500 text-white text-sm px-4 py-3 rounded">
|
||||||
|
Your performance
|
||||||
|
</div>
|
||||||
|
<div className="bg-white p-2">
|
||||||
|
<table className="table table-compact text-gray-500 w-full">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>Trading profit</td>
|
||||||
|
<td>{formatMoney(yourTraderScore ?? 0)}</td>
|
||||||
|
</tr>
|
||||||
|
{yourCreatorScore && (
|
||||||
|
<tr>
|
||||||
|
<td>Created market vol</td>
|
||||||
|
<td>{formatMoney(yourCreatorScore)}</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
)}
|
||||||
|
|
||||||
<Leaderboard
|
<Leaderboard
|
||||||
className="max-w-xl"
|
className="max-w-xl"
|
||||||
title="🏅 Top traders"
|
title="🏅 Top traders"
|
||||||
|
@ -352,13 +408,14 @@ function FoldLeaderboards(props: {
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Leaderboard
|
<Leaderboard
|
||||||
className="max-w-xl"
|
className="max-w-xl"
|
||||||
title="🏅 Top creators"
|
title="🏅 Top creators"
|
||||||
users={topCreators}
|
users={topCreators}
|
||||||
columns={[
|
columns={[
|
||||||
{
|
{
|
||||||
header: 'Market pool',
|
header: 'Market vol',
|
||||||
renderCell: (user) =>
|
renderCell: (user) =>
|
||||||
formatMoney(topCreatorScores[topCreators.indexOf(user)]),
|
formatMoney(topCreatorScores[topCreators.indexOf(user)]),
|
||||||
},
|
},
|
||||||
|
|
|
@ -66,12 +66,12 @@ export default function Folds(props: {
|
||||||
<Col className="max-w-lg w-full">
|
<Col className="max-w-lg w-full">
|
||||||
<Col className="px-4 sm:px-0">
|
<Col className="px-4 sm:px-0">
|
||||||
<Row className="justify-between items-center">
|
<Row className="justify-between items-center">
|
||||||
<Title text="Explore folds" />
|
<Title text="Explore communities" />
|
||||||
{user && <CreateFoldButton />}
|
{user && <CreateFoldButton />}
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
<div className="text-gray-500 mb-6">
|
<div className="text-gray-500 mb-6">
|
||||||
Folds are communities on Manifold centered around a collection of
|
Communities on Manifold are centered around a collection of
|
||||||
markets.
|
markets.
|
||||||
</div>
|
</div>
|
||||||
</Col>
|
</Col>
|
||||||
|
|
|
@ -10,7 +10,7 @@ import {
|
||||||
Comment,
|
Comment,
|
||||||
listAllComments,
|
listAllComments,
|
||||||
} from '../lib/firebase/comments'
|
} from '../lib/firebase/comments'
|
||||||
import { Bet, listAllBets } from '../lib/firebase/bets'
|
import { Bet, getRecentBets, listAllBets } from '../lib/firebase/bets'
|
||||||
import FeedCreate from '../components/feed-create'
|
import FeedCreate from '../components/feed-create'
|
||||||
import { Spacer } from '../components/layout/spacer'
|
import { Spacer } from '../components/layout/spacer'
|
||||||
import { Col } from '../components/layout/col'
|
import { Col } from '../components/layout/col'
|
||||||
|
@ -18,12 +18,17 @@ import { useUser } from '../hooks/use-user'
|
||||||
import { useContracts } from '../hooks/use-contracts'
|
import { useContracts } from '../hooks/use-contracts'
|
||||||
|
|
||||||
export async function getStaticProps() {
|
export async function getStaticProps() {
|
||||||
const [contracts, recentComments] = await Promise.all([
|
const [contracts, recentComments, recentBets] = await Promise.all([
|
||||||
listAllContracts().catch((_) => []),
|
listAllContracts().catch((_) => []),
|
||||||
getRecentComments().catch(() => []),
|
getRecentComments().catch(() => []),
|
||||||
|
getRecentBets().catch(() => []),
|
||||||
])
|
])
|
||||||
|
|
||||||
const activeContracts = findActiveContracts(contracts, recentComments)
|
const activeContracts = findActiveContracts(
|
||||||
|
contracts,
|
||||||
|
recentComments,
|
||||||
|
recentBets
|
||||||
|
)
|
||||||
const activeContractBets = await Promise.all(
|
const activeContractBets = await Promise.all(
|
||||||
activeContracts.map((contract) => listAllBets(contract.id).catch((_) => []))
|
activeContracts.map((contract) => listAllBets(contract.id).catch((_) => []))
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,6 +1,11 @@
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
|
import Textarea from 'react-expanding-textarea'
|
||||||
|
import { parseWordsAsTags } from '../../common/util/parse'
|
||||||
|
import { AmountInput } from '../components/amount-input'
|
||||||
|
import { InfoTooltip } from '../components/info-tooltip'
|
||||||
|
|
||||||
import { Col } from '../components/layout/col'
|
import { Col } from '../components/layout/col'
|
||||||
import { Row } from '../components/layout/row'
|
import { Row } from '../components/layout/row'
|
||||||
|
@ -97,9 +102,18 @@ export default function MakePredictions() {
|
||||||
const user = useUser()
|
const user = useUser()
|
||||||
const [predictionsString, setPredictionsString] = useState('')
|
const [predictionsString, setPredictionsString] = useState('')
|
||||||
const [description, setDescription] = useState('')
|
const [description, setDescription] = useState('')
|
||||||
|
const [tags, setTags] = useState('')
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||||
const [createdContracts, setCreatedContracts] = useState<Contract[]>([])
|
const [createdContracts, setCreatedContracts] = useState<Contract[]>([])
|
||||||
|
|
||||||
|
const [ante, setAnte] = useState<number | undefined>(100)
|
||||||
|
const [anteError, setAnteError] = useState<string | undefined>()
|
||||||
|
// By default, close the market a week from today
|
||||||
|
const weekFromToday = dayjs().add(7, 'day').format('YYYY-MM-DDT23:59')
|
||||||
|
const [closeDate, setCloseDate] = useState<undefined | string>(weekFromToday)
|
||||||
|
|
||||||
|
const closeTime = closeDate ? dayjs(closeDate).valueOf() : undefined
|
||||||
|
|
||||||
const bulkPlaceholder = `e.g.
|
const bulkPlaceholder = `e.g.
|
||||||
${TEST_VALUE}
|
${TEST_VALUE}
|
||||||
...
|
...
|
||||||
|
@ -138,6 +152,9 @@ ${TEST_VALUE}
|
||||||
question: prediction.question,
|
question: prediction.question,
|
||||||
description: prediction.description,
|
description: prediction.description,
|
||||||
initialProb: prediction.initialProb,
|
initialProb: prediction.initialProb,
|
||||||
|
ante,
|
||||||
|
closeTime,
|
||||||
|
tags: parseWordsAsTags(tags),
|
||||||
}).then((r) => (r.data as any).contract)
|
}).then((r) => (r.data as any).contract)
|
||||||
|
|
||||||
setCreatedContracts((prev) => [...prev, contract])
|
setCreatedContracts((prev) => [...prev, contract])
|
||||||
|
@ -171,6 +188,19 @@ ${TEST_VALUE}
|
||||||
|
|
||||||
<Spacer h={4} />
|
<Spacer h={4} />
|
||||||
|
|
||||||
|
<div className="form-control w-full">
|
||||||
|
<label className="label">
|
||||||
|
<span className="label-text">Description</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<Textarea
|
||||||
|
placeholder="e.g. This market is part of the ACX predictions for 2022..."
|
||||||
|
className="input"
|
||||||
|
value={description}
|
||||||
|
onChange={(e) => setDescription(e.target.value || '')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="form-control w-full">
|
<div className="form-control w-full">
|
||||||
<label className="label">
|
<label className="label">
|
||||||
<span className="label-text">Tags</span>
|
<span className="label-text">Tags</span>
|
||||||
|
@ -178,10 +208,46 @@ ${TEST_VALUE}
|
||||||
|
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="e.g. #ACX2021 #World"
|
placeholder="e.g. ACX2021 World"
|
||||||
className="input"
|
className="input"
|
||||||
value={description}
|
value={tags}
|
||||||
onChange={(e) => setDescription(e.target.value || '')}
|
onChange={(e) => setTags(e.target.value || '')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-control items-start mb-1">
|
||||||
|
<label className="label gap-2 mb-1">
|
||||||
|
<span>Market close</span>
|
||||||
|
<InfoTooltip text="Trading will be halted after this time (local timezone)." />
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="datetime-local"
|
||||||
|
className="input input-bordered"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
onChange={(e) => setCloseDate(e.target.value || '')}
|
||||||
|
min={Date.now()}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
value={closeDate}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Spacer h={4} />
|
||||||
|
|
||||||
|
<div className="form-control items-start mb-1">
|
||||||
|
<label className="label gap-2 mb-1">
|
||||||
|
<span>Market ante</span>
|
||||||
|
<InfoTooltip
|
||||||
|
text={`Subsidize your market to encourage trading. Ante bets are set to match your initial probability.
|
||||||
|
You earn ${0.01 * 100}% of trading volume.`}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<AmountInput
|
||||||
|
amount={ante}
|
||||||
|
minimumAmount={10}
|
||||||
|
onChange={setAnte}
|
||||||
|
error={anteError}
|
||||||
|
setError={setAnteError}
|
||||||
|
disabled={isSubmitting}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -226,3 +292,13 @@ ${TEST_VALUE}
|
||||||
</Page>
|
</Page>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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()
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user