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
|
||||
|
||||
.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
|
||||
|
||||
tags: string[]
|
||||
lowercaseTags: string[]
|
||||
|
||||
contractIds: string[]
|
||||
excludedContractIds: string[]
|
||||
|
@ -17,4 +18,6 @@ export type Fold = {
|
|||
excludedCreatorIds?: string[]
|
||||
|
||||
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 { Contract } from '../../../common/contract'
|
||||
import { getPayouts } from '../../../common/payouts'
|
||||
import { Bet } from './bets'
|
||||
import * as _ from 'lodash'
|
||||
import { Bet } from './bet'
|
||||
import { Contract } from './contract'
|
||||
import { getPayouts } from './payouts'
|
||||
|
||||
export function scoreCreators(contracts: Contract[], bets: Bet[][]) {
|
||||
const creatorScore = _.mapValues(
|
||||
|
@ -18,15 +18,12 @@ export function scoreTraders(contracts: Contract[], bets: Bet[][]) {
|
|||
)
|
||||
const userScores: { [userId: string]: number } = {}
|
||||
for (const scores of userScoresByContract) {
|
||||
for (const [userId, score] of Object.entries(scores)) {
|
||||
if (userScores[userId] === undefined) userScores[userId] = 0
|
||||
userScores[userId] += score
|
||||
}
|
||||
addUserScores(scores, userScores)
|
||||
}
|
||||
return userScores
|
||||
}
|
||||
|
||||
function scoreUsersByContract(contract: Contract, bets: Bet[]) {
|
||||
export function scoreUsersByContract(contract: Contract, bets: Bet[]) {
|
||||
const { resolution, resolutionProbability } = contract
|
||||
|
||||
const [closedBets, openBets] = _.partition(
|
||||
|
@ -61,3 +58,13 @@ function scoreUsersByContract(contract: Contract, bets: Bet[]) {
|
|||
|
||||
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) {
|
||||
const taggedText = text
|
||||
.split(/\s+/)
|
||||
.map((tag) => `#${tag}`)
|
||||
.map((tag) => (tag.startsWith('#') ? tag : `#${tag}`))
|
||||
.join(' ')
|
||||
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",
|
||||
"version": "1.0.0",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"watch": "tsc -w",
|
||||
|
|
|
@ -59,6 +59,7 @@ export const createFold = functions.runWith({ minInstances: 1 }).https.onCall(
|
|||
name,
|
||||
about,
|
||||
tags,
|
||||
lowercaseTags: tags.map((tag) => tag.toLowerCase()),
|
||||
createdTime: Date.now(),
|
||||
contractIds: [],
|
||||
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>(
|
||||
firestore.collection('contracts')
|
||||
)
|
||||
|
||||
await Promise.all(
|
||||
contracts.map(async (contract) => {
|
||||
const volume24Hours = await computeVolumeFrom(contract, oneDay)
|
||||
|
|
|
@ -7,7 +7,6 @@ import { Contract } from '../../common/contract'
|
|||
import { Bet } from '../../common/bet'
|
||||
import { User } from '../../common/user'
|
||||
import { calculatePayout } from '../../common/calculate'
|
||||
import { StripeTransaction } from '.'
|
||||
|
||||
const firestore = admin.firestore()
|
||||
|
||||
|
@ -29,16 +28,9 @@ export const updateUserMetrics = functions.pubsub
|
|||
user,
|
||||
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 totalPnL = totalValue - totalDeposits
|
||||
const totalPnL = totalValue - user.totalDeposits
|
||||
|
||||
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
|
||||
)}
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
type="number"
|
||||
placeholder="0"
|
||||
maxLength={9}
|
||||
value={amount ?? ''}
|
||||
|
|
|
@ -4,6 +4,7 @@ export function ConfirmationButton(props: {
|
|||
id: string
|
||||
openModelBtn: {
|
||||
label: string
|
||||
icon?: any
|
||||
className?: string
|
||||
}
|
||||
cancelBtn?: {
|
||||
|
@ -25,7 +26,7 @@ export function ConfirmationButton(props: {
|
|||
htmlFor={id}
|
||||
className={clsx('btn modal-button', openModelBtn.className)}
|
||||
>
|
||||
{openModelBtn.label}
|
||||
{openModelBtn.icon} {openModelBtn.label}
|
||||
</label>
|
||||
<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 createdTime = bets[0].createdTime
|
||||
// Use the time of the last bet for the entire group
|
||||
const createdTime = bets[bets.length - 1].createdTime
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
|
@ -18,16 +18,19 @@ import { ContractFeed } from './contract-feed'
|
|||
import { TweetButton } from './tweet-button'
|
||||
import { Bet } from '../../common/bet'
|
||||
import { Comment } from '../../common/comment'
|
||||
import { TagsInput } from './tags-input'
|
||||
import { RevealableTagsInput, TagsInput } from './tags-input'
|
||||
import BetRow from './bet-row'
|
||||
import { Fold } from '../../common/fold'
|
||||
import { FoldTagList } from './tags-list'
|
||||
|
||||
export const ContractOverview = (props: {
|
||||
contract: Contract
|
||||
bets: Bet[]
|
||||
comments: Comment[]
|
||||
folds: Fold[]
|
||||
className?: string
|
||||
}) => {
|
||||
const { contract, bets, comments, className } = props
|
||||
const { contract, bets, comments, folds, className } = props
|
||||
const { resolution, creatorId, creatorName } = contract
|
||||
const { probPercent, truePool } = contractMetrics(contract)
|
||||
|
||||
|
@ -85,11 +88,28 @@ export const ContractOverview = (props: {
|
|||
|
||||
<ContractProbGraph contract={contract} />
|
||||
|
||||
<Row className="justify-between mt-6 ml-4 gap-4">
|
||||
<TagsInput contract={contract} />
|
||||
<Row className="hidden sm:flex justify-between items-center mt-6 ml-4 gap-4">
|
||||
{folds.length === 0 ? (
|
||||
<TagsInput className={clsx('mx-4')} contract={contract} />
|
||||
) : (
|
||||
<FoldTagList folds={folds} />
|
||||
)}
|
||||
<TweetButton tweetText={tweetText} />
|
||||
</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} />
|
||||
|
||||
{/* Show a delete button for contracts without any trading */}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import clsx from 'clsx'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useState } from 'react'
|
||||
import { PlusCircleIcon } from '@heroicons/react/solid'
|
||||
import { parseWordsAsTags } from '../../common/util/parse'
|
||||
import { createFold } from '../lib/firebase/api-call'
|
||||
import { foldPath } from '../lib/firebase/folds'
|
||||
|
@ -49,7 +50,8 @@ export function CreateFoldButton() {
|
|||
<ConfirmationButton
|
||||
id="create-fold"
|
||||
openModelBtn={{
|
||||
label: 'Create a fold',
|
||||
label: 'New',
|
||||
icon: <PlusCircleIcon className="w-5 h-5 mr-2" />,
|
||||
className: clsx(
|
||||
isSubmitting ? 'loading btn-disabled' : 'btn-primary',
|
||||
'btn-sm'
|
||||
|
@ -61,11 +63,11 @@ export function CreateFoldButton() {
|
|||
}}
|
||||
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">
|
||||
<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>
|
||||
</Col>
|
||||
|
||||
|
@ -74,11 +76,11 @@ export function CreateFoldButton() {
|
|||
<div>
|
||||
<div className="form-control w-full">
|
||||
<label className="label">
|
||||
<span className="mb-1">Fold name</span>
|
||||
<span className="mb-1">Community name</span>
|
||||
</label>
|
||||
|
||||
<input
|
||||
placeholder="Your fold name"
|
||||
placeholder="Name"
|
||||
className="input input-bordered resize-none"
|
||||
disabled={isSubmitting}
|
||||
value={name}
|
||||
|
@ -109,7 +111,7 @@ export function CreateFoldButton() {
|
|||
<label className="label">
|
||||
<span className="mb-1">Primary tag</span>
|
||||
</label>
|
||||
<TagsList noLink tags={[`#${toCamelCase(name)}`]} />
|
||||
<TagsList noLink noLabel tags={[`#${toCamelCase(name)}`]} />
|
||||
|
||||
<Spacer h={4} />
|
||||
|
||||
|
@ -132,6 +134,7 @@ export function CreateFoldButton() {
|
|||
<TagsList
|
||||
tags={parseWordsAsTags(otherTags).map((tag) => `#${tag}`)}
|
||||
noLink
|
||||
noLabel
|
||||
/>
|
||||
</div>
|
||||
</ConfirmationButton>
|
||||
|
|
|
@ -59,7 +59,7 @@ export function EditFoldButton(props: { fold: Fold; className?: string }) {
|
|||
<div className="modal-box">
|
||||
<div className="form-control w-full">
|
||||
<label className="label">
|
||||
<span className="mb-1">Fold name</span>
|
||||
<span className="mb-1">Community name</span>
|
||||
</label>
|
||||
|
||||
<input
|
||||
|
@ -105,7 +105,7 @@ export function EditFoldButton(props: { fold: Fold; className?: string }) {
|
|||
</div>
|
||||
|
||||
<Spacer h={4} />
|
||||
<TagsList tags={tags.map((tag) => `#${tag}`)} noLink />
|
||||
<TagsList tags={tags.map((tag) => `#${tag}`)} noLink noLabel />
|
||||
<Spacer h={4} />
|
||||
|
||||
<div className="modal-action">
|
||||
|
|
|
@ -81,7 +81,7 @@ export default function FeedCreate(props: {
|
|||
|
||||
return (
|
||||
<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()}
|
||||
>
|
||||
<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
|
||||
return (
|
||||
<div className={clsx('w-full px-1', className)}>
|
||||
<Title text={title} />
|
||||
<Title text={title} className="!mt-0" />
|
||||
{users.length === 0 ? (
|
||||
<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 { ManifoldLogo } from './manifold-logo'
|
||||
import { ProfileMenu } from './profile-menu'
|
||||
import {
|
||||
CollectionIcon,
|
||||
HomeIcon,
|
||||
SearchIcon,
|
||||
UserGroupIcon,
|
||||
} from '@heroicons/react/outline'
|
||||
|
||||
export function NavBar(props: {
|
||||
darkBackground?: boolean
|
||||
|
@ -22,25 +28,76 @@ export function NavBar(props: {
|
|||
const themeClasses = clsx(darkBackground && 'text-white', hoverClasses)
|
||||
|
||||
return (
|
||||
<nav className={clsx('w-full p-4 mb-4', className)} aria-label="Global">
|
||||
<Row
|
||||
className={clsx(
|
||||
'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}
|
||||
/>
|
||||
<>
|
||||
<nav className={clsx('w-full p-4 mb-4', className)} aria-label="Global">
|
||||
<Row
|
||||
className={clsx(
|
||||
'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}
|
||||
/>
|
||||
)}
|
||||
</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>
|
||||
)
|
||||
}
|
||||
|
@ -77,7 +134,7 @@ function NavOptions(props: {
|
|||
themeClasses
|
||||
)}
|
||||
>
|
||||
Folds
|
||||
Communities
|
||||
</a>
|
||||
</Link>
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@ export function Page(props: {
|
|||
|
||||
<div
|
||||
className={clsx(
|
||||
'w-full mx-auto',
|
||||
'w-full mx-auto pb-16',
|
||||
wide ? 'max-w-6xl' : 'max-w-4xl',
|
||||
margin && 'px-4'
|
||||
)}
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
import clsx from 'clsx'
|
||||
import { useState } from 'react'
|
||||
import { parseWordsAsTags } from '../../common/util/parse'
|
||||
import { Contract, updateContract } from '../lib/firebase/contracts'
|
||||
import { Col } from './layout/col'
|
||||
import { Row } from './layout/row'
|
||||
import { TagsList } from './tags-list'
|
||||
|
||||
export function TagsInput(props: { contract: Contract }) {
|
||||
const { contract } = props
|
||||
export function TagsInput(props: { contract: Contract; className?: string }) {
|
||||
const { contract, className } = props
|
||||
const { tags } = contract
|
||||
|
||||
const [tagText, setTagText] = useState('')
|
||||
|
@ -24,7 +26,7 @@ export function TagsInput(props: { contract: Contract }) {
|
|||
}
|
||||
|
||||
return (
|
||||
<Row className="flex-wrap gap-4">
|
||||
<Col className={clsx('gap-4', className)}>
|
||||
<TagsList tags={newTags.map((tag) => `#${tag}`)} />
|
||||
|
||||
<Row className="items-center gap-4">
|
||||
|
@ -40,6 +42,28 @@ export function TagsInput(props: { contract: Contract }) {
|
|||
Save tags
|
||||
</button>
|
||||
</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 { Row } from './layout/row'
|
||||
import { Linkify } from './linkify'
|
||||
import { SiteLink } from './site-link'
|
||||
import { Fold } from '../../common/fold'
|
||||
|
||||
export function Hashtag(props: { tag: string; noLink?: boolean }) {
|
||||
const { tag, noLink } = props
|
||||
const body = (
|
||||
<div
|
||||
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'
|
||||
)}
|
||||
>
|
||||
<span className="text-gray-500">{tag}</span>
|
||||
<span className="text-gray-600 text-sm">{tag}</span>
|
||||
</div>
|
||||
)
|
||||
|
||||
|
@ -28,10 +28,12 @@ export function TagsList(props: {
|
|||
tags: string[]
|
||||
className?: string
|
||||
noLink?: boolean
|
||||
noLabel?: boolean
|
||||
}) {
|
||||
const { tags, className, noLink } = props
|
||||
const { tags, className, noLink, noLabel } = props
|
||||
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) => (
|
||||
<Hashtag key={tag} tag={tag} noLink={noLink} />
|
||||
))}
|
||||
|
@ -39,15 +41,35 @@ export function TagsList(props: {
|
|||
)
|
||||
}
|
||||
|
||||
export function CompactTagsList(props: { tags: string[] }) {
|
||||
const { tags } = props
|
||||
export function FoldTag(props: { fold: Fold }) {
|
||||
const { fold } = props
|
||||
const { name } = fold
|
||||
return (
|
||||
<Row className="gap-2 flex-wrap text-sm text-gray-500">
|
||||
{tags.map((tag) => (
|
||||
<div key={tag} className="bg-gray-100 px-1">
|
||||
<Linkify text={tag} gray />
|
||||
</div>
|
||||
))}
|
||||
<SiteLink href={`/fold/${fold.slug}`} className="flex items-center">
|
||||
<div
|
||||
className={clsx(
|
||||
'bg-white border-2 px-4 py-1 rounded-full shadow-md',
|
||||
'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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import {
|
|||
query,
|
||||
onSnapshot,
|
||||
where,
|
||||
orderBy,
|
||||
} from 'firebase/firestore'
|
||||
import _ from 'lodash'
|
||||
|
||||
|
@ -23,6 +24,29 @@ export async function listAllBets(contractId: string) {
|
|||
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(
|
||||
contractId: string,
|
||||
setBets: (bets: Bet[]) => void
|
||||
|
|
|
@ -7,6 +7,7 @@ import {
|
|||
updateDoc,
|
||||
where,
|
||||
} from 'firebase/firestore'
|
||||
import _ from 'lodash'
|
||||
import { Fold } from '../../../common/fold'
|
||||
import { Contract, contractCollection } from './contracts'
|
||||
import { db } from './init'
|
||||
|
@ -131,3 +132,18 @@ export function listenForFollow(
|
|||
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 { 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.NODE_ENV === 'production'
|
||||
export const isProd = true
|
||||
export const isProd = process.env.NEXT_PUBLIC_FIREBASE_ENV !== 'DEV'
|
||||
|
||||
const firebaseConfig = isProd
|
||||
? {
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
{
|
||||
"name": "mantic",
|
||||
"name": "web",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"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",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
|
|
|
@ -23,6 +23,8 @@ import { contractTextDetails } from '../../components/contract-card'
|
|||
import { Bet, listAllBets } from '../../lib/firebase/bets'
|
||||
import { Comment, listAllComments } from '../../lib/firebase/comments'
|
||||
import Custom404 from '../404'
|
||||
import { getFoldsByTags } from '../../lib/firebase/folds'
|
||||
import { Fold } from '../../../common/fold'
|
||||
|
||||
export async function getStaticProps(props: {
|
||||
params: { username: string; contractSlug: string }
|
||||
|
@ -31,18 +33,23 @@ export async function getStaticProps(props: {
|
|||
const contract = (await getContractFromSlug(contractSlug)) || null
|
||||
const contractId = contract?.id
|
||||
|
||||
const foldsPromise = getFoldsByTags(contract?.tags ?? [])
|
||||
|
||||
const [bets, comments] = await Promise.all([
|
||||
contractId ? listAllBets(contractId) : null,
|
||||
contractId ? listAllComments(contractId) : null,
|
||||
contractId ? listAllBets(contractId) : [],
|
||||
contractId ? listAllComments(contractId) : [],
|
||||
])
|
||||
|
||||
const folds = await foldsPromise
|
||||
|
||||
return {
|
||||
props: {
|
||||
contract,
|
||||
username,
|
||||
slug: contractSlug,
|
||||
contract,
|
||||
bets,
|
||||
comments,
|
||||
folds,
|
||||
},
|
||||
|
||||
revalidate: 60, // regenerate after a minute
|
||||
|
@ -55,15 +62,16 @@ export async function getStaticPaths() {
|
|||
|
||||
export default function ContractPage(props: {
|
||||
contract: Contract | null
|
||||
bets: Bet[] | null
|
||||
comments: Comment[] | null
|
||||
slug: string
|
||||
username: string
|
||||
bets: Bet[]
|
||||
comments: Comment[]
|
||||
slug: string
|
||||
folds: Fold[]
|
||||
}) {
|
||||
const user = useUser()
|
||||
|
||||
const contract = useContractWithPreload(props.slug, props.contract)
|
||||
const { bets, comments } = props
|
||||
const { bets, comments, folds } = props
|
||||
|
||||
if (!contract) {
|
||||
return <Custom404 />
|
||||
|
@ -103,6 +111,7 @@ export default function ContractPage(props: {
|
|||
contract={contract}
|
||||
bets={bets ?? []}
|
||||
comments={comments ?? []}
|
||||
folds={folds}
|
||||
/>
|
||||
<BetsSection contract={contract} user={user ?? null} />
|
||||
</div>
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { cloneElement } from 'react'
|
||||
import { Page } from '../components/page'
|
||||
import { SEO } from '../components/SEO'
|
||||
import { useContracts } from '../hooks/use-contracts'
|
||||
import styles from './about.module.css'
|
||||
|
||||
export default function About() {
|
||||
|
@ -244,7 +245,18 @@ function Contents() {
|
|||
</li>
|
||||
<li>
|
||||
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>
|
||||
Chat:{' '}
|
||||
|
|
|
@ -7,6 +7,7 @@ import { Col } from '../components/layout/col'
|
|||
import { Bet } from '../../common/bet'
|
||||
|
||||
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.
|
||||
// TODO: Maybe store last activity time directly in the contract?
|
||||
|
@ -23,9 +24,11 @@ function lastActivityTime(contract: Contract) {
|
|||
// - Comment on a market
|
||||
// - New market created
|
||||
// - Market resolved
|
||||
// - Markets with most betting in last 24 hours
|
||||
export function findActiveContracts(
|
||||
allContracts: Contract[],
|
||||
recentComments: Comment[],
|
||||
recentBets: Bet[],
|
||||
daysAgo = 3
|
||||
) {
|
||||
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 = contracts.filter((contract) => contract.visibility === 'public')
|
||||
contracts = _.sortBy(contracts, (c) => -(idToActivityTime.get(c.id) ?? 0))
|
||||
|
|
|
@ -5,7 +5,11 @@ import { Fold } from '../../../../common/fold'
|
|||
import { Comment } from '../../../../common/comment'
|
||||
import { Page } from '../../../components/page'
|
||||
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 { Contract } from '../../../lib/firebase/contracts'
|
||||
import {
|
||||
|
@ -26,7 +30,7 @@ import { SearchableGrid } from '../../../components/contracts-list'
|
|||
import { useQueryAndSortParams } from '../../../hooks/use-sort-and-query-params'
|
||||
import { useRouter } from 'next/router'
|
||||
import clsx from 'clsx'
|
||||
import { scoreCreators, scoreTraders } from '../../../lib/firebase/scoring'
|
||||
import { scoreCreators, scoreTraders } from '../../../../common/scoring'
|
||||
import { Leaderboard } from '../../../components/leaderboard'
|
||||
import { formatMoney, toCamelCase } from '../../../../common/util/format'
|
||||
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 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(
|
||||
contracts,
|
||||
_.flatten(contractComments),
|
||||
_.flatten(contractRecentBets),
|
||||
365
|
||||
)
|
||||
const [resolved, unresolved] = _.partition(
|
||||
|
@ -66,17 +83,16 @@ export async function getStaticProps(props: { params: { slugs: string[] } }) {
|
|||
contractComments[contracts.findIndex((c) => c.id === contract.id)]
|
||||
)
|
||||
|
||||
const curator = await curatorPromise
|
||||
|
||||
const bets = await Promise.all(
|
||||
contracts.map((contract) => listAllBets(contract.id))
|
||||
)
|
||||
const bets = await betsPromise
|
||||
|
||||
const creatorScores = scoreCreators(contracts, bets)
|
||||
const [topCreators, topCreatorScores] = await toUserScores(creatorScores)
|
||||
|
||||
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 {
|
||||
props: {
|
||||
|
@ -86,17 +102,17 @@ export async function getStaticProps(props: { params: { slugs: string[] } }) {
|
|||
activeContracts,
|
||||
activeContractBets,
|
||||
activeContractComments,
|
||||
traderScores,
|
||||
topTraders,
|
||||
topTraderScores,
|
||||
creatorScores,
|
||||
topCreators,
|
||||
topCreatorScores,
|
||||
},
|
||||
|
||||
revalidate: 60, // regenerate after a minute
|
||||
}
|
||||
}
|
||||
|
||||
async function toUserScores(userScores: { [userId: string]: number }) {
|
||||
async function toTopUsers(userScores: { [userId: string]: number }) {
|
||||
const topUserPairs = _.take(
|
||||
_.sortBy(Object.entries(userScores), ([_, score]) => -1 * score),
|
||||
10
|
||||
|
@ -105,14 +121,7 @@ async function toUserScores(userScores: { [userId: string]: number }) {
|
|||
const topUsers = await Promise.all(
|
||||
topUserPairs.map(([userId]) => getUser(userId))
|
||||
)
|
||||
const existingPairs = topUserPairs.filter(([id, _]) =>
|
||||
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
|
||||
return topUsers.filter((user) => user)
|
||||
}
|
||||
|
||||
export async function getStaticPaths() {
|
||||
|
@ -127,19 +136,19 @@ export default function FoldPage(props: {
|
|||
activeContracts: Contract[]
|
||||
activeContractBets: Bet[][]
|
||||
activeContractComments: Comment[][]
|
||||
traderScores: { [userId: string]: number }
|
||||
topTraders: User[]
|
||||
topTraderScores: number[]
|
||||
creatorScores: { [userId: string]: number }
|
||||
topCreators: User[]
|
||||
topCreatorScores: number[]
|
||||
}) {
|
||||
const {
|
||||
curator,
|
||||
activeContractBets,
|
||||
activeContractComments,
|
||||
traderScores,
|
||||
topTraders,
|
||||
topTraderScores,
|
||||
creatorScores,
|
||||
topCreators,
|
||||
topCreatorScores,
|
||||
} = props
|
||||
|
||||
const router = useRouter()
|
||||
|
@ -239,9 +248,9 @@ export default function FoldPage(props: {
|
|||
{(page === 'activity' || page === 'markets') && (
|
||||
<Row className={clsx(page === 'activity' ? 'gap-16' : 'gap-8')}>
|
||||
<Col className="flex-1">
|
||||
{user !== null && (
|
||||
{user !== null && !fold.disallowMarketCreation && (
|
||||
<FeedCreate
|
||||
className={clsx(page !== 'activity' && 'hidden')}
|
||||
className={clsx('border-b-2', page !== 'activity' && 'hidden')}
|
||||
user={user}
|
||||
tag={toCamelCase(fold.name)}
|
||||
placeholder={`Type your question about ${fold.name}`}
|
||||
|
@ -271,25 +280,28 @@ export default function FoldPage(props: {
|
|||
/>
|
||||
)}
|
||||
</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} />
|
||||
<FoldLeaderboards
|
||||
traderScores={traderScores}
|
||||
creatorScores={creatorScores}
|
||||
topTraders={topTraders}
|
||||
topTraderScores={topTraderScores}
|
||||
topCreators={topCreators}
|
||||
topCreatorScores={topCreatorScores}
|
||||
user={user}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
|
||||
{page === 'leaderboards' && (
|
||||
<Col className="gap-8 lg:flex-row">
|
||||
<Col className="gap-8 lg:flex-row px-4">
|
||||
<FoldLeaderboards
|
||||
traderScores={traderScores}
|
||||
creatorScores={creatorScores}
|
||||
topTraders={topTraders}
|
||||
topTraderScores={topTraderScores}
|
||||
topCreators={topCreators}
|
||||
topCreatorScores={topCreatorScores}
|
||||
user={user}
|
||||
yourPerformanceClassName="lg:hidden"
|
||||
/>
|
||||
</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>
|
||||
)
|
||||
}
|
||||
|
||||
function FoldLeaderboards(props: {
|
||||
traderScores: { [userId: string]: number }
|
||||
creatorScores: { [userId: string]: number }
|
||||
topTraders: User[]
|
||||
topTraderScores: number[]
|
||||
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 (
|
||||
<>
|
||||
{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
|
||||
className="max-w-xl"
|
||||
title="🏅 Top traders"
|
||||
|
@ -352,13 +408,14 @@ function FoldLeaderboards(props: {
|
|||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
<Leaderboard
|
||||
className="max-w-xl"
|
||||
title="🏅 Top creators"
|
||||
users={topCreators}
|
||||
columns={[
|
||||
{
|
||||
header: 'Market pool',
|
||||
header: 'Market vol',
|
||||
renderCell: (user) =>
|
||||
formatMoney(topCreatorScores[topCreators.indexOf(user)]),
|
||||
},
|
||||
|
|
|
@ -66,12 +66,12 @@ export default function Folds(props: {
|
|||
<Col className="max-w-lg w-full">
|
||||
<Col className="px-4 sm:px-0">
|
||||
<Row className="justify-between items-center">
|
||||
<Title text="Explore folds" />
|
||||
<Title text="Explore communities" />
|
||||
{user && <CreateFoldButton />}
|
||||
</Row>
|
||||
|
||||
<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.
|
||||
</div>
|
||||
</Col>
|
||||
|
|
|
@ -10,7 +10,7 @@ import {
|
|||
Comment,
|
||||
listAllComments,
|
||||
} 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 { Spacer } from '../components/layout/spacer'
|
||||
import { Col } from '../components/layout/col'
|
||||
|
@ -18,12 +18,17 @@ import { useUser } from '../hooks/use-user'
|
|||
import { useContracts } from '../hooks/use-contracts'
|
||||
|
||||
export async function getStaticProps() {
|
||||
const [contracts, recentComments] = await Promise.all([
|
||||
const [contracts, recentComments, recentBets] = await Promise.all([
|
||||
listAllContracts().catch((_) => []),
|
||||
getRecentComments().catch(() => []),
|
||||
getRecentBets().catch(() => []),
|
||||
])
|
||||
|
||||
const activeContracts = findActiveContracts(contracts, recentComments)
|
||||
const activeContracts = findActiveContracts(
|
||||
contracts,
|
||||
recentComments,
|
||||
recentBets
|
||||
)
|
||||
const activeContractBets = await Promise.all(
|
||||
activeContracts.map((contract) => listAllBets(contract.id).catch((_) => []))
|
||||
)
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
import clsx from 'clsx'
|
||||
import dayjs from 'dayjs'
|
||||
import Link from 'next/link'
|
||||
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 { Row } from '../components/layout/row'
|
||||
|
@ -97,9 +102,18 @@ export default function MakePredictions() {
|
|||
const user = useUser()
|
||||
const [predictionsString, setPredictionsString] = useState('')
|
||||
const [description, setDescription] = useState('')
|
||||
const [tags, setTags] = useState('')
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
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.
|
||||
${TEST_VALUE}
|
||||
...
|
||||
|
@ -138,6 +152,9 @@ ${TEST_VALUE}
|
|||
question: prediction.question,
|
||||
description: prediction.description,
|
||||
initialProb: prediction.initialProb,
|
||||
ante,
|
||||
closeTime,
|
||||
tags: parseWordsAsTags(tags),
|
||||
}).then((r) => (r.data as any).contract)
|
||||
|
||||
setCreatedContracts((prev) => [...prev, contract])
|
||||
|
@ -171,6 +188,19 @@ ${TEST_VALUE}
|
|||
|
||||
<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">
|
||||
<label className="label">
|
||||
<span className="label-text">Tags</span>
|
||||
|
@ -178,10 +208,46 @@ ${TEST_VALUE}
|
|||
|
||||
<input
|
||||
type="text"
|
||||
placeholder="e.g. #ACX2021 #World"
|
||||
placeholder="e.g. ACX2021 World"
|
||||
className="input"
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value || '')}
|
||||
value={tags}
|
||||
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>
|
||||
|
||||
|
@ -226,3 +292,13 @@ ${TEST_VALUE}
|
|||
</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