Merge branch 'main' into CPM-ui
This commit is contained in:
commit
4ac1cfa357
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -1,4 +1,4 @@
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
.idea/
|
||||||
.vercel
|
.vercel
|
||||||
node_modules
|
node_modules
|
||||||
|
|
|
@ -18,7 +18,14 @@ import {
|
||||||
getDpmProbabilityAfterSale,
|
getDpmProbabilityAfterSale,
|
||||||
} from './calculate-dpm'
|
} from './calculate-dpm'
|
||||||
import { calculateFixedPayout } from './calculate-fixed-payouts'
|
import { calculateFixedPayout } from './calculate-fixed-payouts'
|
||||||
import { Binary, Contract, CPMM, DPM, FullContract } from './contract'
|
import {
|
||||||
|
Binary,
|
||||||
|
Contract,
|
||||||
|
CPMM,
|
||||||
|
DPM,
|
||||||
|
FreeResponseContract,
|
||||||
|
FullContract,
|
||||||
|
} from './contract'
|
||||||
|
|
||||||
export function getProbability(contract: FullContract<DPM | CPMM, Binary>) {
|
export function getProbability(contract: FullContract<DPM | CPMM, Binary>) {
|
||||||
return contract.mechanism === 'cpmm-1'
|
return contract.mechanism === 'cpmm-1'
|
||||||
|
@ -170,3 +177,15 @@ export function getContractBetNullMetrics() {
|
||||||
profitPercent: 0,
|
profitPercent: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getTopAnswer(contract: FreeResponseContract) {
|
||||||
|
const { answers } = contract
|
||||||
|
const top = _.maxBy(
|
||||||
|
answers.map((answer) => ({
|
||||||
|
answer,
|
||||||
|
prob: getOutcomeProbability(contract, answer.id),
|
||||||
|
})),
|
||||||
|
({ prob }) => prob
|
||||||
|
)
|
||||||
|
return top?.answer
|
||||||
|
}
|
||||||
|
|
|
@ -10,3 +10,9 @@ export type ClickEvent = {
|
||||||
contractId: string
|
contractId: string
|
||||||
timestamp: number
|
timestamp: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type LatencyEvent = {
|
||||||
|
type: 'feed' | 'portfolio'
|
||||||
|
latency: number
|
||||||
|
timestamp: number
|
||||||
|
}
|
||||||
|
|
|
@ -30,6 +30,10 @@ service cloud.firestore {
|
||||||
allow create: if userId == request.auth.uid;
|
allow create: if userId == request.auth.uid;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
match /private-users/{userId}/latency/{loadTimeId} {
|
||||||
|
allow create: if userId == request.auth.uid;
|
||||||
|
}
|
||||||
|
|
||||||
match /contracts/{contractId} {
|
match /contracts/{contractId} {
|
||||||
allow read;
|
allow read;
|
||||||
allow update: if request.resource.data.diff(resource.data).affectedKeys()
|
allow update: if request.resource.data.diff(resource.data).affectedKeys()
|
||||||
|
|
|
@ -17,16 +17,16 @@ Adapted from https://firebase.google.com/docs/functions/get-started
|
||||||
0. `$ cd functions` to switch to this folder
|
0. `$ cd functions` to switch to this folder
|
||||||
1. `$ yarn global add firebase-tools` to install the Firebase CLI globally
|
1. `$ yarn global add firebase-tools` to install the Firebase CLI globally
|
||||||
2. `$ yarn` to install JS dependencies
|
2. `$ yarn` to install JS dependencies
|
||||||
3. `$ firebase functions:config:get > .runtimeconfig.json` to cache secrets for local dev (TODO: maybe not for Manifold)
|
3. `$ firebase login` to authenticate the CLI tools to Firebase
|
||||||
4. `$ firebase use dev` to choose the dev project
|
4. `$ firebase use dev` to choose the dev project
|
||||||
|
5. `$ firebase functions:config:get > .runtimeconfig.json` to cache secrets for local dev (TODO: maybe not for Manifold)
|
||||||
|
|
||||||
## Developing locally
|
## Developing locally
|
||||||
|
|
||||||
1. `$ firebase login` if you aren't logged into Firebase via commandline yet.
|
0. `$ yarn dev` to spin up the emulators
|
||||||
2. `$ yarn dev` to spin up the emulators
|
|
||||||
The Emulator UI is at http://localhost:4000; the functions are hosted on :5001.
|
The Emulator UI is at http://localhost:4000; the functions are hosted on :5001.
|
||||||
Note: You have to kill and restart emulators when you change code; no hot reload =(
|
Note: You have to kill and restart emulators when you change code; no hot reload =(
|
||||||
3. Connect to emulators by enabling `functions.useEmulator('localhost', 5001)`
|
1. Connect to emulators by enabling `functions.useEmulator('localhost', 5001)`
|
||||||
|
|
||||||
## Debugging
|
## Debugging
|
||||||
|
|
||||||
|
|
|
@ -1,15 +1,8 @@
|
||||||
import * as admin from 'firebase-admin'
|
import * as admin from 'firebase-admin'
|
||||||
import * as _ from 'lodash'
|
import * as _ from 'lodash'
|
||||||
|
|
||||||
// Generate your own private key, and set the path below:
|
import { initAdmin } from './script-init'
|
||||||
// https://console.firebase.google.com/u/0/project/mantic-markets/settings/serviceaccounts/adminsdk
|
initAdmin()
|
||||||
|
|
||||||
// const serviceAccount = require('../../../../../../Downloads/dev-mantic-markets-firebase-adminsdk-sir5m-b2d27f8970.json')
|
|
||||||
const serviceAccount = require('../../../../../../Downloads/mantic-markets-firebase-adminsdk-1ep46-351a65eca3.json')
|
|
||||||
|
|
||||||
admin.initializeApp({
|
|
||||||
credential: admin.credential.cert(serviceAccount),
|
|
||||||
})
|
|
||||||
|
|
||||||
import { getUserByUsername } from '../utils'
|
import { getUserByUsername } from '../utils'
|
||||||
import { changeUser } from '../change-user-info'
|
import { changeUser } from '../change-user-info'
|
||||||
|
|
|
@ -2,7 +2,7 @@ import * as admin from 'firebase-admin'
|
||||||
import * as _ from 'lodash'
|
import * as _ from 'lodash'
|
||||||
|
|
||||||
import { initAdmin } from './script-init'
|
import { initAdmin } from './script-init'
|
||||||
initAdmin('stephen')
|
initAdmin()
|
||||||
|
|
||||||
import { Bet } from '../../../common/bet'
|
import { Bet } from '../../../common/bet'
|
||||||
import { getDpmProbability } from '../../../common/calculate-dpm'
|
import { getDpmProbability } from '../../../common/calculate-dpm'
|
||||||
|
|
|
@ -2,7 +2,7 @@ import * as admin from 'firebase-admin'
|
||||||
import * as _ from 'lodash'
|
import * as _ from 'lodash'
|
||||||
|
|
||||||
import { initAdmin } from './script-init'
|
import { initAdmin } from './script-init'
|
||||||
initAdmin('stephen')
|
initAdmin()
|
||||||
|
|
||||||
import { PrivateUser, STARTING_BALANCE, User } from '../../../common/user'
|
import { PrivateUser, STARTING_BALANCE, User } from '../../../common/user'
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ import * as _ from 'lodash'
|
||||||
import * as fs from 'fs'
|
import * as fs from 'fs'
|
||||||
|
|
||||||
import { initAdmin } from './script-init'
|
import { initAdmin } from './script-init'
|
||||||
initAdmin('james')
|
initAdmin()
|
||||||
|
|
||||||
import { Bet } from '../../../common/bet'
|
import { Bet } from '../../../common/bet'
|
||||||
import { Contract } from '../../../common/contract'
|
import { Contract } from '../../../common/contract'
|
||||||
|
|
|
@ -2,7 +2,7 @@ import * as admin from 'firebase-admin'
|
||||||
import * as _ from 'lodash'
|
import * as _ from 'lodash'
|
||||||
|
|
||||||
import { initAdmin } from './script-init'
|
import { initAdmin } from './script-init'
|
||||||
initAdmin('james')
|
initAdmin()
|
||||||
|
|
||||||
import { getValues } from '../utils'
|
import { getValues } from '../utils'
|
||||||
import { Fold } from '../../../common/fold'
|
import { Fold } from '../../../common/fold'
|
||||||
|
|
|
@ -2,7 +2,7 @@ import * as admin from 'firebase-admin'
|
||||||
import * as _ from 'lodash'
|
import * as _ from 'lodash'
|
||||||
|
|
||||||
import { initAdmin } from './script-init'
|
import { initAdmin } from './script-init'
|
||||||
initAdmin('james')
|
initAdmin()
|
||||||
|
|
||||||
import { Contract } from '../../../common/contract'
|
import { Contract } from '../../../common/contract'
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ import * as admin from 'firebase-admin'
|
||||||
import * as _ from 'lodash'
|
import * as _ from 'lodash'
|
||||||
|
|
||||||
import { initAdmin } from './script-init'
|
import { initAdmin } from './script-init'
|
||||||
initAdmin('james')
|
initAdmin()
|
||||||
|
|
||||||
import { Bet } from '../../../common/bet'
|
import { Bet } from '../../../common/bet'
|
||||||
import { Contract } from '../../../common/contract'
|
import { Contract } from '../../../common/contract'
|
||||||
|
|
|
@ -2,7 +2,7 @@ import * as admin from 'firebase-admin'
|
||||||
import * as _ from 'lodash'
|
import * as _ from 'lodash'
|
||||||
|
|
||||||
import { initAdmin } from './script-init'
|
import { initAdmin } from './script-init'
|
||||||
initAdmin('stephenDev')
|
initAdmin()
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Binary,
|
Binary,
|
||||||
|
|
|
@ -2,7 +2,7 @@ import * as admin from 'firebase-admin'
|
||||||
import * as _ from 'lodash'
|
import * as _ from 'lodash'
|
||||||
|
|
||||||
import { initAdmin } from './script-init'
|
import { initAdmin } from './script-init'
|
||||||
initAdmin('stephenDev')
|
initAdmin()
|
||||||
|
|
||||||
import { Binary, Contract, DPM, FullContract } from '../../../common/contract'
|
import { Binary, Contract, DPM, FullContract } from '../../../common/contract'
|
||||||
import { Bet } from '../../../common/bet'
|
import { Bet } from '../../../common/bet'
|
||||||
|
|
|
@ -2,7 +2,7 @@ import * as admin from 'firebase-admin'
|
||||||
import * as _ from 'lodash'
|
import * as _ from 'lodash'
|
||||||
|
|
||||||
import { initAdmin } from './script-init'
|
import { initAdmin } from './script-init'
|
||||||
initAdmin('james')
|
initAdmin()
|
||||||
|
|
||||||
import { Bet } from '../../../common/bet'
|
import { Bet } from '../../../common/bet'
|
||||||
import { Contract } from '../../../common/contract'
|
import { Contract } from '../../../common/contract'
|
||||||
|
|
|
@ -2,7 +2,7 @@ import * as admin from 'firebase-admin'
|
||||||
import * as _ from 'lodash'
|
import * as _ from 'lodash'
|
||||||
|
|
||||||
import { initAdmin } from './script-init'
|
import { initAdmin } from './script-init'
|
||||||
initAdmin('james')
|
initAdmin()
|
||||||
|
|
||||||
import { Bet } from '../../../common/bet'
|
import { Bet } from '../../../common/bet'
|
||||||
import { Contract } from '../../../common/contract'
|
import { Contract } from '../../../common/contract'
|
||||||
|
|
|
@ -2,7 +2,7 @@ import * as admin from 'firebase-admin'
|
||||||
import * as _ from 'lodash'
|
import * as _ from 'lodash'
|
||||||
|
|
||||||
import { initAdmin } from './script-init'
|
import { initAdmin } from './script-init'
|
||||||
initAdmin('james')
|
initAdmin()
|
||||||
|
|
||||||
import { Bet } from '../../../common/bet'
|
import { Bet } from '../../../common/bet'
|
||||||
import { Contract } from '../../../common/contract'
|
import { Contract } from '../../../common/contract'
|
||||||
|
|
|
@ -2,7 +2,7 @@ import * as admin from 'firebase-admin'
|
||||||
import * as _ from 'lodash'
|
import * as _ from 'lodash'
|
||||||
|
|
||||||
import { initAdmin } from './script-init'
|
import { initAdmin } from './script-init'
|
||||||
initAdmin('stephenDev')
|
initAdmin()
|
||||||
|
|
||||||
import { Contract } from '../../../common/contract'
|
import { Contract } from '../../../common/contract'
|
||||||
import { getValues } from '../utils'
|
import { getValues } from '../utils'
|
||||||
|
|
|
@ -1,32 +1,71 @@
|
||||||
|
import * as path from 'path'
|
||||||
|
import * as fs from 'fs'
|
||||||
|
import * as os from 'os'
|
||||||
import * as admin from 'firebase-admin'
|
import * as admin from 'firebase-admin'
|
||||||
|
|
||||||
// Generate your own private key, and set the path below:
|
// First, generate a private key from the Google service account management page:
|
||||||
// Prod:
|
// Prod: https://console.firebase.google.com/u/0/project/mantic-markets/settings/serviceaccounts/adminsdk
|
||||||
// https://console.firebase.google.com/u/0/project/mantic-markets/settings/serviceaccounts/adminsdk
|
// Dev: https://console.firebase.google.com/u/0/project/dev-mantic-markets/settings/serviceaccounts/adminsdk
|
||||||
// Dev:
|
// Then set GOOGLE_APPLICATION_CREDENTIALS_PROD or GOOGLE_APPLICATION_CREDENTIALS_DEV to the path of the key.
|
||||||
// https://console.firebase.google.com/u/0/project/dev-mantic-markets/settings/serviceaccounts/adminsdk
|
|
||||||
|
|
||||||
const pathsToPrivateKey = {
|
// Then, to run a script, make sure you are pointing at the Firebase you intend to:
|
||||||
james:
|
// $ firebase use dev (or prod)
|
||||||
'/Users/jahooma/mantic-markets-firebase-adminsdk-1ep46-820891bb87.json',
|
//
|
||||||
jamesDev:
|
// Followed by, if you have https://github.com/TypeStrong/ts-node installed (recommended):
|
||||||
'/Users/jahooma/dev-mantic-markets-firebase-adminsdk-sir5m-f38cdbee37.json',
|
// $ ts-node my-script.ts
|
||||||
stephen:
|
//
|
||||||
'../../../../../../Downloads/mantic-markets-firebase-adminsdk-1ep46-351a65eca3.json',
|
// Or compile it and run the compiled version:
|
||||||
stephenDev:
|
// $ yarn build && ../../lib/functions/scripts/src/my-script.js
|
||||||
'../../../../../../Downloads/dev-mantic-markets-firebase-adminsdk-sir5m-b2d27f8970.json',
|
|
||||||
|
const getFirebaseProjectRoot = (cwd: string) => {
|
||||||
|
// see https://github.com/firebase/firebase-tools/blob/master/src/detectProjectRoot.ts
|
||||||
|
let dir = cwd
|
||||||
|
while (!fs.existsSync(path.resolve(dir, './firebase.json'))) {
|
||||||
|
const parentDir = path.dirname(dir)
|
||||||
|
if (parentDir === dir) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
dir = parentDir
|
||||||
|
}
|
||||||
|
return dir
|
||||||
}
|
}
|
||||||
|
|
||||||
export const initAdmin = (who: keyof typeof pathsToPrivateKey) => {
|
const getFirebaseActiveProject = (cwd: string) => {
|
||||||
const serviceAccount = require(pathsToPrivateKey[who])
|
// firebase uses this configstore package https://github.com/yeoman/configstore/blob/main/index.js#L9
|
||||||
|
const projectRoot = getFirebaseProjectRoot(cwd)
|
||||||
|
if (projectRoot == null) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
const xdgConfig =
|
||||||
|
process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config')
|
||||||
|
const configPath = path.join(xdgConfig, 'configstore', 'firebase-tools.json')
|
||||||
|
try {
|
||||||
|
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'))
|
||||||
|
return config['activeProjects'][projectRoot]
|
||||||
|
} catch (e) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const initAdmin = (env?: string) => {
|
||||||
|
env = env || getFirebaseActiveProject(process.cwd())
|
||||||
|
if (env == null) {
|
||||||
|
console.error(
|
||||||
|
"Couldn't find active Firebase project; did you do `firebase use <alias>?`"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const envVar = `GOOGLE_APPLICATION_CREDENTIALS_${env.toUpperCase()}`
|
||||||
|
const keyPath = process.env[envVar]
|
||||||
|
if (keyPath == null) {
|
||||||
|
console.error(
|
||||||
|
`Please set the ${envVar} environment variable to contain the path to your ${env} environment key file.`
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
console.log(`Initializing connection to ${env} Firebase...`)
|
||||||
|
const serviceAccount = require(keyPath)
|
||||||
admin.initializeApp({
|
admin.initializeApp({
|
||||||
credential: admin.credential.cert(serviceAccount),
|
credential: admin.credential.cert(serviceAccount),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Then:
|
|
||||||
// yarn watch (or yarn build)
|
|
||||||
// firebase use dev (or firebase use prod)
|
|
||||||
// Run script:
|
|
||||||
// node lib/functions/src/scripts/update-contract-tags.js
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ import * as admin from 'firebase-admin'
|
||||||
import * as _ from 'lodash'
|
import * as _ from 'lodash'
|
||||||
|
|
||||||
import { initAdmin } from './script-init'
|
import { initAdmin } from './script-init'
|
||||||
initAdmin('jamesDev')
|
initAdmin()
|
||||||
|
|
||||||
import { Contract } from '../../../common/contract'
|
import { Contract } from '../../../common/contract'
|
||||||
import { parseTags } from '../../../common/util/parse'
|
import { parseTags } from '../../../common/util/parse'
|
||||||
|
|
|
@ -64,6 +64,7 @@ export function AnswerBetPanel(props: {
|
||||||
if (result?.status === 'success') {
|
if (result?.status === 'success') {
|
||||||
setIsSubmitting(false)
|
setIsSubmitting(false)
|
||||||
setBetAmount(undefined)
|
setBetAmount(undefined)
|
||||||
|
props.closePanel()
|
||||||
} else {
|
} else {
|
||||||
setError(result?.error || 'Error placing bet')
|
setError(result?.error || 'Error placing bet')
|
||||||
setIsSubmitting(false)
|
setIsSubmitting(false)
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import _ from 'lodash'
|
|
||||||
|
|
||||||
import { useUser } from '../hooks/use-user'
|
import { useUser } from '../hooks/use-user'
|
||||||
import { Binary, CPMM, DPM, FullContract } from '../../common/contract'
|
import { Binary, CPMM, DPM, FullContract } from '../../common/contract'
|
||||||
|
@ -19,7 +18,7 @@ import { Bet } from '../../common/bet'
|
||||||
import { placeBet, sellShares } from '../lib/firebase/api-call'
|
import { placeBet, sellShares } from '../lib/firebase/api-call'
|
||||||
import { BuyAmountInput, SellAmountInput } from './amount-input'
|
import { BuyAmountInput, SellAmountInput } from './amount-input'
|
||||||
import { InfoTooltip } from './info-tooltip'
|
import { InfoTooltip } from './info-tooltip'
|
||||||
import { BinaryOutcomeLabel, OutcomeLabel } from './outcome-label'
|
import { BinaryOutcomeLabel } from './outcome-label'
|
||||||
import {
|
import {
|
||||||
calculatePayoutAfterCorrectBet,
|
calculatePayoutAfterCorrectBet,
|
||||||
calculateShares,
|
calculateShares,
|
||||||
|
@ -32,61 +31,30 @@ import {
|
||||||
calculateCpmmSale,
|
calculateCpmmSale,
|
||||||
getCpmmProbability,
|
getCpmmProbability,
|
||||||
} from '../../common/calculate-cpmm'
|
} from '../../common/calculate-cpmm'
|
||||||
import { Modal } from './layout/modal'
|
import { SellRow } from './sell-row'
|
||||||
|
import { useSaveShares } from './use-save-shares'
|
||||||
|
|
||||||
export function BetPanel(props: {
|
export function BetPanel(props: {
|
||||||
contract: FullContract<DPM | CPMM, Binary>
|
contract: FullContract<DPM | CPMM, Binary>
|
||||||
className?: string
|
className?: string
|
||||||
}) {
|
}) {
|
||||||
const { contract, className } = props
|
const { contract, className } = props
|
||||||
const { mechanism } = contract
|
|
||||||
|
|
||||||
const user = useUser()
|
const user = useUser()
|
||||||
const userBets = useUserContractBets(user?.id, contract.id)
|
const userBets = useUserContractBets(user?.id, contract.id)
|
||||||
|
const { yesFloorShares, noFloorShares } = useSaveShares(contract, userBets)
|
||||||
const [showSellModal, setShowSellModal] = useState(false)
|
const sharesOutcome = yesFloorShares
|
||||||
|
? 'YES'
|
||||||
const { yesShares, noShares } = useSaveShares(contract, userBets)
|
: noFloorShares
|
||||||
|
? 'NO'
|
||||||
const shares = yesShares || noShares
|
: undefined
|
||||||
const sharesOutcome = yesShares ? 'YES' : noShares ? 'NO' : undefined
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Col className={className}>
|
<Col className={className}>
|
||||||
{sharesOutcome && user && mechanism === 'cpmm-1' && (
|
<SellRow
|
||||||
<Col className="rounded-t-md bg-gray-100 px-6 py-6">
|
contract={contract}
|
||||||
<Row className="items-center justify-between gap-2">
|
user={user}
|
||||||
<div>
|
className={'rounded-t-md bg-gray-100 px-6 py-6'}
|
||||||
You have {formatWithCommas(Math.floor(shares))}{' '}
|
/>
|
||||||
<BinaryOutcomeLabel outcome={sharesOutcome} /> shares
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
className="btn btn-sm"
|
|
||||||
style={{
|
|
||||||
backgroundColor: 'white',
|
|
||||||
border: '2px solid',
|
|
||||||
color: '#3D4451',
|
|
||||||
}}
|
|
||||||
onClick={() => setShowSellModal(true)}
|
|
||||||
>
|
|
||||||
Sell
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{showSellModal && (
|
|
||||||
<SellSharesModal
|
|
||||||
contract={contract as FullContract<CPMM, Binary>}
|
|
||||||
user={user}
|
|
||||||
userBets={userBets ?? []}
|
|
||||||
shares={shares}
|
|
||||||
sharesOutcome={sharesOutcome}
|
|
||||||
setOpen={setShowSellModal}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Row>
|
|
||||||
</Col>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Col
|
<Col
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'rounded-b-md bg-white px-8 py-6',
|
'rounded-b-md bg-white px-8 py-6',
|
||||||
|
@ -128,10 +96,17 @@ export function BetPanelSwitcher(props: {
|
||||||
|
|
||||||
const [tradeType, setTradeType] = useState<'BUY' | 'SELL'>('BUY')
|
const [tradeType, setTradeType] = useState<'BUY' | 'SELL'>('BUY')
|
||||||
|
|
||||||
const { yesShares, noShares } = useSaveShares(contract, userBets)
|
const { yesFloorShares, noFloorShares, yesShares, noShares } = useSaveShares(
|
||||||
|
contract,
|
||||||
|
userBets
|
||||||
|
)
|
||||||
|
|
||||||
const shares = yesShares || noShares
|
const floorShares = yesFloorShares || noFloorShares
|
||||||
const sharesOutcome = yesShares ? 'YES' : noShares ? 'NO' : undefined
|
const sharesOutcome = yesFloorShares
|
||||||
|
? 'YES'
|
||||||
|
: noFloorShares
|
||||||
|
? 'NO'
|
||||||
|
: undefined
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Switch back to BUY if the user has sold all their shares.
|
// Switch back to BUY if the user has sold all their shares.
|
||||||
|
@ -146,7 +121,7 @@ export function BetPanelSwitcher(props: {
|
||||||
<Col className="rounded-t-md bg-gray-100 px-6 py-6">
|
<Col className="rounded-t-md bg-gray-100 px-6 py-6">
|
||||||
<Row className="items-center justify-between gap-2">
|
<Row className="items-center justify-between gap-2">
|
||||||
<div>
|
<div>
|
||||||
You have {formatWithCommas(Math.floor(shares))}{' '}
|
You have {formatWithCommas(floorShares)}{' '}
|
||||||
<BinaryOutcomeLabel outcome={sharesOutcome} /> shares
|
<BinaryOutcomeLabel outcome={sharesOutcome} /> shares
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -394,7 +369,7 @@ function BuyPanel(props: {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function SellPanel(props: {
|
export function SellPanel(props: {
|
||||||
contract: FullContract<CPMM, Binary>
|
contract: FullContract<CPMM, Binary>
|
||||||
userBets: Bet[]
|
userBets: Bet[]
|
||||||
shares: number
|
shares: number
|
||||||
|
@ -493,78 +468,3 @@ function SellPanel(props: {
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const useSaveShares = (
|
|
||||||
contract: FullContract<CPMM | DPM, Binary>,
|
|
||||||
userBets: Bet[] | undefined
|
|
||||||
) => {
|
|
||||||
const [savedShares, setSavedShares] = useState<
|
|
||||||
{ yesShares: number; noShares: number } | undefined
|
|
||||||
>()
|
|
||||||
|
|
||||||
const [yesBets, noBets] = _.partition(
|
|
||||||
userBets ?? [],
|
|
||||||
(bet) => bet.outcome === 'YES'
|
|
||||||
)
|
|
||||||
const [yesShares, noShares] = [
|
|
||||||
_.sumBy(yesBets, (bet) => bet.shares),
|
|
||||||
_.sumBy(noBets, (bet) => bet.shares),
|
|
||||||
]
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// Save yes and no shares to local storage.
|
|
||||||
const savedShares = localStorage.getItem(`${contract.id}-shares`)
|
|
||||||
if (!userBets && savedShares) {
|
|
||||||
setSavedShares(JSON.parse(savedShares))
|
|
||||||
}
|
|
||||||
|
|
||||||
if (userBets) {
|
|
||||||
const updatedShares = { yesShares, noShares }
|
|
||||||
localStorage.setItem(
|
|
||||||
`${contract.id}-shares`,
|
|
||||||
JSON.stringify(updatedShares)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}, [contract.id, userBets, noShares, yesShares])
|
|
||||||
|
|
||||||
if (userBets) return { yesShares, noShares }
|
|
||||||
return savedShares ?? { yesShares: 0, noShares: 0 }
|
|
||||||
}
|
|
||||||
|
|
||||||
function SellSharesModal(props: {
|
|
||||||
contract: FullContract<CPMM, Binary>
|
|
||||||
userBets: Bet[]
|
|
||||||
shares: number
|
|
||||||
sharesOutcome: 'YES' | 'NO'
|
|
||||||
user: User
|
|
||||||
setOpen: (open: boolean) => void
|
|
||||||
}) {
|
|
||||||
const { contract, shares, sharesOutcome, userBets, user, setOpen } = props
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal open={true} setOpen={setOpen}>
|
|
||||||
<Col className="rounded-md bg-white px-8 py-6">
|
|
||||||
<Title className="!mt-0" text={'Sell shares'} />
|
|
||||||
|
|
||||||
<div className="mb-6">
|
|
||||||
You have {formatWithCommas(Math.floor(shares))}{' '}
|
|
||||||
<OutcomeLabel
|
|
||||||
outcome={sharesOutcome}
|
|
||||||
contract={contract}
|
|
||||||
truncate="long"
|
|
||||||
/>{' '}
|
|
||||||
shares
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<SellPanel
|
|
||||||
contract={contract}
|
|
||||||
shares={shares}
|
|
||||||
sharesOutcome={sharesOutcome}
|
|
||||||
user={user}
|
|
||||||
userBets={userBets ?? []}
|
|
||||||
onSellSuccess={() => setOpen(false)}
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
</Modal>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import clsx from 'clsx'
|
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
|
|
||||||
import { BetPanelSwitcher } from './bet-panel'
|
import { BetPanelSwitcher } from './bet-panel'
|
||||||
|
@ -6,6 +5,10 @@ import { Row } from './layout/row'
|
||||||
import { YesNoSelector } from './yes-no-selector'
|
import { YesNoSelector } from './yes-no-selector'
|
||||||
import { Binary, CPMM, DPM, FullContract } from '../../common/contract'
|
import { Binary, CPMM, DPM, FullContract } from '../../common/contract'
|
||||||
import { Modal } from './layout/modal'
|
import { Modal } from './layout/modal'
|
||||||
|
import { SellButton } from './sell-button'
|
||||||
|
import { useUser } from '../hooks/use-user'
|
||||||
|
import { useUserContractBets } from '../hooks/use-user-bets'
|
||||||
|
import { useSaveShares } from './use-save-shares'
|
||||||
|
|
||||||
// Inline version of a bet panel. Opens BetPanel in a new modal.
|
// Inline version of a bet panel. Opens BetPanel in a new modal.
|
||||||
export default function BetRow(props: {
|
export default function BetRow(props: {
|
||||||
|
@ -13,16 +16,19 @@ export default function BetRow(props: {
|
||||||
className?: string
|
className?: string
|
||||||
labelClassName?: string
|
labelClassName?: string
|
||||||
}) {
|
}) {
|
||||||
const { className, labelClassName } = props
|
const { className, labelClassName, contract } = props
|
||||||
const [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
const [betChoice, setBetChoice] = useState<'YES' | 'NO' | undefined>(
|
const [betChoice, setBetChoice] = useState<'YES' | 'NO' | undefined>(
|
||||||
undefined
|
undefined
|
||||||
)
|
)
|
||||||
|
const user = useUser()
|
||||||
|
const userBets = useUserContractBets(user?.id, contract.id)
|
||||||
|
const { yesFloorShares, noFloorShares } = useSaveShares(contract, userBets)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={className}>
|
<div className={className}>
|
||||||
<Row className="items-center justify-end gap-2">
|
<Row className="mt-2 justify-end space-x-3">
|
||||||
{/* <div className={clsx('mr-2 text-gray-400', labelClassName)}>
|
{/* <div className={clsx('mr-2 text-gray-400', labelClassName)}>
|
||||||
Place a trade
|
Place a trade
|
||||||
</div> */}
|
</div> */}
|
||||||
|
@ -32,12 +38,22 @@ export default function BetRow(props: {
|
||||||
setOpen(true)
|
setOpen(true)
|
||||||
setBetChoice(choice)
|
setBetChoice(choice)
|
||||||
}}
|
}}
|
||||||
|
replaceNoButton={
|
||||||
|
yesFloorShares > noFloorShares && yesFloorShares > 0 ? (
|
||||||
|
<SellButton contract={contract} user={user} />
|
||||||
|
) : undefined
|
||||||
|
}
|
||||||
|
replaceYesButton={
|
||||||
|
noFloorShares > yesFloorShares && noFloorShares > 0 ? (
|
||||||
|
<SellButton contract={contract} user={user} />
|
||||||
|
) : undefined
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</Row>
|
</Row>
|
||||||
<Modal open={open} setOpen={setOpen}>
|
<Modal open={open} setOpen={setOpen}>
|
||||||
<BetPanelSwitcher
|
<BetPanelSwitcher
|
||||||
contract={props.contract}
|
contract={contract}
|
||||||
title={props.contract.question}
|
title={contract.question}
|
||||||
selected={betChoice}
|
selected={betChoice}
|
||||||
onBetSuccess={() => setOpen(false)}
|
onBetSuccess={() => setOpen(false)}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -37,6 +37,8 @@ import {
|
||||||
resolvedPayout,
|
resolvedPayout,
|
||||||
getContractBetNullMetrics,
|
getContractBetNullMetrics,
|
||||||
} from '../../common/calculate'
|
} from '../../common/calculate'
|
||||||
|
import { useTimeSinceFirstRender } from '../hooks/use-time-since-first-render'
|
||||||
|
import { trackLatency } from '../lib/firebase/tracking'
|
||||||
|
|
||||||
type BetSort = 'newest' | 'profit' | 'closeTime' | 'value'
|
type BetSort = 'newest' | 'profit' | 'closeTime' | 'value'
|
||||||
type BetFilter = 'open' | 'closed' | 'resolved' | 'all'
|
type BetFilter = 'open' | 'closed' | 'resolved' | 'all'
|
||||||
|
@ -67,6 +69,14 @@ export function BetsList(props: { user: User }) {
|
||||||
}
|
}
|
||||||
}, [bets])
|
}, [bets])
|
||||||
|
|
||||||
|
const getTime = useTimeSinceFirstRender()
|
||||||
|
useEffect(() => {
|
||||||
|
if (bets && contracts) {
|
||||||
|
trackLatency('portfolio', getTime())
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [!!bets, !!contracts])
|
||||||
|
|
||||||
if (!bets || !contracts) {
|
if (!bets || !contracts) {
|
||||||
return <LoadingIndicator />
|
return <LoadingIndicator />
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,27 +1,15 @@
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
import { ClockIcon, DatabaseIcon, PencilIcon } from '@heroicons/react/outline'
|
|
||||||
import { TrendingUpIcon } from '@heroicons/react/solid'
|
|
||||||
import { Row } from '../layout/row'
|
import { Row } from '../layout/row'
|
||||||
import { formatMoney, formatPercent } from '../../../common/util/format'
|
import { formatPercent } from '../../../common/util/format'
|
||||||
import { UserLink } from '../user-page'
|
|
||||||
import {
|
import {
|
||||||
Contract,
|
Contract,
|
||||||
contractMetrics,
|
|
||||||
contractPath,
|
contractPath,
|
||||||
getBinaryProbPercent,
|
getBinaryProbPercent,
|
||||||
updateContract,
|
|
||||||
} from '../../lib/firebase/contracts'
|
} from '../../lib/firebase/contracts'
|
||||||
import { Col } from '../layout/col'
|
import { Col } from '../layout/col'
|
||||||
import dayjs from 'dayjs'
|
|
||||||
import { DateTimeTooltip } from '../datetime-tooltip'
|
|
||||||
import { fromNow } from '../../lib/util/time'
|
|
||||||
import { Avatar } from '../avatar'
|
|
||||||
import { Spacer } from '../layout/spacer'
|
import { Spacer } from '../layout/spacer'
|
||||||
import { useState } from 'react'
|
|
||||||
import { ContractInfoDialog } from './contract-info-dialog'
|
|
||||||
import { Bet } from '../../../common/bet'
|
|
||||||
import {
|
import {
|
||||||
Binary,
|
Binary,
|
||||||
CPMM,
|
CPMM,
|
||||||
|
@ -35,7 +23,8 @@ import {
|
||||||
BinaryContractOutcomeLabel,
|
BinaryContractOutcomeLabel,
|
||||||
FreeResponseOutcomeLabel,
|
FreeResponseOutcomeLabel,
|
||||||
} from '../outcome-label'
|
} from '../outcome-label'
|
||||||
import { getOutcomeProbability } from '../../../common/calculate'
|
import { getOutcomeProbability, getTopAnswer } from '../../../common/calculate'
|
||||||
|
import { AbbrContractDetails } from './contract-details'
|
||||||
|
|
||||||
export function ContractCard(props: {
|
export function ContractCard(props: {
|
||||||
contract: Contract
|
contract: Contract
|
||||||
|
@ -68,7 +57,7 @@ export function ContractCard(props: {
|
||||||
<Row
|
<Row
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'justify-between gap-4',
|
'justify-between gap-4',
|
||||||
outcomeType === 'FREE_RESPONSE' && 'flex-col items-start'
|
outcomeType === 'FREE_RESPONSE' && 'flex-col items-start !gap-2'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<p
|
<p
|
||||||
|
@ -85,6 +74,7 @@ export function ContractCard(props: {
|
||||||
)}
|
)}
|
||||||
{outcomeType === 'FREE_RESPONSE' && (
|
{outcomeType === 'FREE_RESPONSE' && (
|
||||||
<FreeResponseResolutionOrChance
|
<FreeResponseResolutionOrChance
|
||||||
|
className="self-end text-gray-600"
|
||||||
contract={contract as FullContract<DPM, FreeResponse>}
|
contract={contract as FullContract<DPM, FreeResponse>}
|
||||||
truncate="long"
|
truncate="long"
|
||||||
/>
|
/>
|
||||||
|
@ -132,29 +122,18 @@ export function BinaryResolutionOrChance(props: {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTopAnswer(contract: FreeResponseContract) {
|
|
||||||
const { answers } = contract
|
|
||||||
const top = _.maxBy(
|
|
||||||
answers.map((answer) => ({
|
|
||||||
answer,
|
|
||||||
prob: getOutcomeProbability(contract, answer.id),
|
|
||||||
})),
|
|
||||||
({ prob }) => prob
|
|
||||||
)
|
|
||||||
return top?.answer
|
|
||||||
}
|
|
||||||
|
|
||||||
export function FreeResponseResolutionOrChance(props: {
|
export function FreeResponseResolutionOrChance(props: {
|
||||||
contract: FreeResponseContract
|
contract: FreeResponseContract
|
||||||
truncate: 'short' | 'long' | 'none'
|
truncate: 'short' | 'long' | 'none'
|
||||||
|
className?: string
|
||||||
}) {
|
}) {
|
||||||
const { contract, truncate } = props
|
const { contract, truncate, className } = props
|
||||||
const { resolution } = contract
|
const { resolution } = contract
|
||||||
|
|
||||||
const topAnswer = getTopAnswer(contract)
|
const topAnswer = getTopAnswer(contract)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Col className="text-xl">
|
<Col className={clsx(resolution ? 'text-3xl' : 'text-xl', className)}>
|
||||||
{resolution ? (
|
{resolution ? (
|
||||||
<>
|
<>
|
||||||
<div className={clsx('text-base text-gray-500')}>Resolved</div>
|
<div className={clsx('text-base text-gray-500')}>Resolved</div>
|
||||||
|
@ -162,13 +141,18 @@ export function FreeResponseResolutionOrChance(props: {
|
||||||
contract={contract}
|
contract={contract}
|
||||||
resolution={resolution}
|
resolution={resolution}
|
||||||
truncate={truncate}
|
truncate={truncate}
|
||||||
|
answerClassName="text-xl"
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
topAnswer && (
|
topAnswer && (
|
||||||
<Row className="flex-1 items-center justify-between gap-6">
|
<Row className="items-center gap-6">
|
||||||
<AnswerLabel answer={topAnswer} truncate={truncate} />
|
<AnswerLabel
|
||||||
<Col className="text-primary">
|
className="!text-gray-600"
|
||||||
|
answer={topAnswer}
|
||||||
|
truncate={truncate}
|
||||||
|
/>
|
||||||
|
<Col className="text-primary text-3xl">
|
||||||
<div>
|
<div>
|
||||||
{formatPercent(getOutcomeProbability(contract, topAnswer.id))}
|
{formatPercent(getOutcomeProbability(contract, topAnswer.id))}
|
||||||
</div>
|
</div>
|
||||||
|
@ -180,209 +164,3 @@ export function FreeResponseResolutionOrChance(props: {
|
||||||
</Col>
|
</Col>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function AbbrContractDetails(props: {
|
|
||||||
contract: Contract
|
|
||||||
showHotVolume?: boolean
|
|
||||||
showCloseTime?: boolean
|
|
||||||
}) {
|
|
||||||
const { contract, showHotVolume, showCloseTime } = props
|
|
||||||
const { volume24Hours, creatorName, creatorUsername, closeTime } = contract
|
|
||||||
const { volumeLabel } = contractMetrics(contract)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Col className={clsx('gap-2 text-sm text-gray-500')}>
|
|
||||||
<Row className="items-center justify-between">
|
|
||||||
<Row className="items-center gap-2">
|
|
||||||
<Avatar
|
|
||||||
username={creatorUsername}
|
|
||||||
avatarUrl={contract.creatorAvatarUrl}
|
|
||||||
size={6}
|
|
||||||
/>
|
|
||||||
<UserLink
|
|
||||||
className="whitespace-nowrap"
|
|
||||||
name={creatorName}
|
|
||||||
username={creatorUsername}
|
|
||||||
/>
|
|
||||||
</Row>
|
|
||||||
|
|
||||||
{showHotVolume ? (
|
|
||||||
<Row className="gap-1">
|
|
||||||
<TrendingUpIcon className="h-5 w-5" /> {formatMoney(volume24Hours)}
|
|
||||||
</Row>
|
|
||||||
) : showCloseTime ? (
|
|
||||||
<Row className="gap-1">
|
|
||||||
<ClockIcon className="h-5 w-5" />
|
|
||||||
{(closeTime || 0) < Date.now() ? 'Closed' : 'Closes'}{' '}
|
|
||||||
{fromNow(closeTime || 0)}
|
|
||||||
</Row>
|
|
||||||
) : (
|
|
||||||
<Row className="gap-1">
|
|
||||||
{/* <DatabaseIcon className="h-5 w-5" /> */}
|
|
||||||
{volumeLabel}
|
|
||||||
</Row>
|
|
||||||
)}
|
|
||||||
</Row>
|
|
||||||
</Col>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ContractDetails(props: {
|
|
||||||
contract: Contract
|
|
||||||
bets: Bet[]
|
|
||||||
isCreator?: boolean
|
|
||||||
hideShareButtons?: boolean
|
|
||||||
}) {
|
|
||||||
const { contract, bets, isCreator, hideShareButtons } = props
|
|
||||||
const { closeTime, creatorName, creatorUsername } = contract
|
|
||||||
const { volumeLabel, createdDate, resolvedDate } = contractMetrics(contract)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Col className="gap-2 text-sm text-gray-500 sm:flex-row sm:flex-wrap">
|
|
||||||
<Row className="flex-1 flex-wrap items-center gap-x-4 gap-y-3">
|
|
||||||
<Row className="items-center gap-2">
|
|
||||||
<Avatar
|
|
||||||
username={creatorUsername}
|
|
||||||
avatarUrl={contract.creatorAvatarUrl}
|
|
||||||
size={6}
|
|
||||||
/>
|
|
||||||
<UserLink
|
|
||||||
className="whitespace-nowrap"
|
|
||||||
name={creatorName}
|
|
||||||
username={creatorUsername}
|
|
||||||
/>
|
|
||||||
</Row>
|
|
||||||
|
|
||||||
{(!!closeTime || !!resolvedDate) && (
|
|
||||||
<Row className="items-center gap-1">
|
|
||||||
<ClockIcon className="h-5 w-5" />
|
|
||||||
|
|
||||||
{/* <DateTimeTooltip text="Market created:" time={contract.createdTime}>
|
|
||||||
{createdDate}
|
|
||||||
</DateTimeTooltip> */}
|
|
||||||
|
|
||||||
{resolvedDate && contract.resolutionTime ? (
|
|
||||||
<>
|
|
||||||
{/* {' - '} */}
|
|
||||||
<DateTimeTooltip
|
|
||||||
text="Market resolved:"
|
|
||||||
time={contract.resolutionTime}
|
|
||||||
>
|
|
||||||
{resolvedDate}
|
|
||||||
</DateTimeTooltip>
|
|
||||||
</>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{!resolvedDate && closeTime && (
|
|
||||||
<>
|
|
||||||
{/* {' - '}{' '} */}
|
|
||||||
<EditableCloseDate
|
|
||||||
closeTime={closeTime}
|
|
||||||
contract={contract}
|
|
||||||
isCreator={isCreator ?? false}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Row>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Row className="items-center gap-1">
|
|
||||||
<DatabaseIcon className="h-5 w-5" />
|
|
||||||
|
|
||||||
<div className="whitespace-nowrap">{volumeLabel}</div>
|
|
||||||
</Row>
|
|
||||||
|
|
||||||
{!hideShareButtons && (
|
|
||||||
<ContractInfoDialog contract={contract} bets={bets} />
|
|
||||||
)}
|
|
||||||
</Row>
|
|
||||||
</Col>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// String version of the above, to send to the OpenGraph image generator
|
|
||||||
export function contractTextDetails(contract: Contract) {
|
|
||||||
const { closeTime, tags } = contract
|
|
||||||
const { createdDate, resolvedDate, volumeLabel } = contractMetrics(contract)
|
|
||||||
|
|
||||||
const hashtags = tags.map((tag) => `#${tag}`)
|
|
||||||
|
|
||||||
return (
|
|
||||||
`${resolvedDate ? `${createdDate} - ${resolvedDate}` : createdDate}` +
|
|
||||||
(closeTime
|
|
||||||
? ` • ${closeTime > Date.now() ? 'Closes' : 'Closed'} ${dayjs(
|
|
||||||
closeTime
|
|
||||||
).format('MMM D, h:mma')}`
|
|
||||||
: '') +
|
|
||||||
` • ${volumeLabel}` +
|
|
||||||
(hashtags.length > 0 ? ` • ${hashtags.join(' ')}` : '')
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function EditableCloseDate(props: {
|
|
||||||
closeTime: number
|
|
||||||
contract: Contract
|
|
||||||
isCreator: boolean
|
|
||||||
}) {
|
|
||||||
const { closeTime, contract, isCreator } = props
|
|
||||||
|
|
||||||
const [isEditingCloseTime, setIsEditingCloseTime] = useState(false)
|
|
||||||
const [closeDate, setCloseDate] = useState(
|
|
||||||
closeTime && dayjs(closeTime).format('YYYY-MM-DDT23:59')
|
|
||||||
)
|
|
||||||
|
|
||||||
const onSave = () => {
|
|
||||||
const newCloseTime = dayjs(closeDate).valueOf()
|
|
||||||
if (newCloseTime === closeTime) setIsEditingCloseTime(false)
|
|
||||||
else if (newCloseTime > Date.now()) {
|
|
||||||
const { description } = contract
|
|
||||||
const formattedCloseDate = dayjs(newCloseTime).format('YYYY-MM-DD h:mm a')
|
|
||||||
const newDescription = `${description}\n\nClose date updated to ${formattedCloseDate}`
|
|
||||||
|
|
||||||
updateContract(contract.id, {
|
|
||||||
closeTime: newCloseTime,
|
|
||||||
description: newDescription,
|
|
||||||
})
|
|
||||||
|
|
||||||
setIsEditingCloseTime(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{isEditingCloseTime ? (
|
|
||||||
<div className="form-control mr-1 items-start">
|
|
||||||
<input
|
|
||||||
type="datetime-local"
|
|
||||||
className="input input-bordered"
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
onChange={(e) => setCloseDate(e.target.value || '')}
|
|
||||||
min={Date.now()}
|
|
||||||
value={closeDate}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<DateTimeTooltip
|
|
||||||
text={closeTime > Date.now() ? 'Trading ends:' : 'Trading ended:'}
|
|
||||||
time={closeTime}
|
|
||||||
>
|
|
||||||
{dayjs(closeTime).format('MMM D')} ({fromNow(closeTime)})
|
|
||||||
</DateTimeTooltip>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{isCreator &&
|
|
||||||
(isEditingCloseTime ? (
|
|
||||||
<button className="btn btn-xs" onClick={onSave}>
|
|
||||||
Done
|
|
||||||
</button>
|
|
||||||
) : (
|
|
||||||
<button
|
|
||||||
className="btn btn-xs btn-ghost"
|
|
||||||
onClick={() => setIsEditingCloseTime(true)}
|
|
||||||
>
|
|
||||||
<PencilIcon className="mr-2 inline h-4 w-4" /> Edit
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
233
web/components/contract/contract-details.tsx
Normal file
233
web/components/contract/contract-details.tsx
Normal file
|
@ -0,0 +1,233 @@
|
||||||
|
import clsx from 'clsx'
|
||||||
|
import _ from 'lodash'
|
||||||
|
import { ClockIcon, DatabaseIcon, PencilIcon } from '@heroicons/react/outline'
|
||||||
|
import { TrendingUpIcon } from '@heroicons/react/solid'
|
||||||
|
import { Row } from '../layout/row'
|
||||||
|
import { formatMoney } from '../../../common/util/format'
|
||||||
|
import { UserLink } from '../user-page'
|
||||||
|
import {
|
||||||
|
Contract,
|
||||||
|
contractMetrics,
|
||||||
|
updateContract,
|
||||||
|
} from '../../lib/firebase/contracts'
|
||||||
|
import { Col } from '../layout/col'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
import { DateTimeTooltip } from '../datetime-tooltip'
|
||||||
|
import { fromNow } from '../../lib/util/time'
|
||||||
|
import { Avatar } from '../avatar'
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { ContractInfoDialog } from './contract-info-dialog'
|
||||||
|
import { Bet } from '../../../common/bet'
|
||||||
|
import NewContractBadge from '../new-contract-badge'
|
||||||
|
|
||||||
|
export function AbbrContractDetails(props: {
|
||||||
|
contract: Contract
|
||||||
|
showHotVolume?: boolean
|
||||||
|
showCloseTime?: boolean
|
||||||
|
}) {
|
||||||
|
const { contract, showHotVolume, showCloseTime } = props
|
||||||
|
const { volume, volume24Hours, creatorName, creatorUsername, closeTime } =
|
||||||
|
contract
|
||||||
|
const { volumeLabel } = contractMetrics(contract)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Col className={clsx('gap-2 text-sm text-gray-500')}>
|
||||||
|
<Row className="items-center justify-between">
|
||||||
|
<Row className="items-center gap-2">
|
||||||
|
<Avatar
|
||||||
|
username={creatorUsername}
|
||||||
|
avatarUrl={contract.creatorAvatarUrl}
|
||||||
|
size={6}
|
||||||
|
/>
|
||||||
|
<UserLink
|
||||||
|
className="whitespace-nowrap"
|
||||||
|
name={creatorName}
|
||||||
|
username={creatorUsername}
|
||||||
|
/>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
{showHotVolume ? (
|
||||||
|
<Row className="gap-1">
|
||||||
|
<TrendingUpIcon className="h-5 w-5" /> {formatMoney(volume24Hours)}
|
||||||
|
</Row>
|
||||||
|
) : showCloseTime ? (
|
||||||
|
<Row className="gap-1">
|
||||||
|
<ClockIcon className="h-5 w-5" />
|
||||||
|
{(closeTime || 0) < Date.now() ? 'Closed' : 'Closes'}{' '}
|
||||||
|
{fromNow(closeTime || 0)}
|
||||||
|
</Row>
|
||||||
|
) : volume > 0 ? (
|
||||||
|
<Row>{volumeLabel}</Row>
|
||||||
|
) : (
|
||||||
|
<NewContractBadge />
|
||||||
|
)}
|
||||||
|
</Row>
|
||||||
|
</Col>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ContractDetails(props: {
|
||||||
|
contract: Contract
|
||||||
|
bets: Bet[]
|
||||||
|
isCreator?: boolean
|
||||||
|
hideShareButtons?: boolean
|
||||||
|
}) {
|
||||||
|
const { contract, bets, isCreator, hideShareButtons } = props
|
||||||
|
const { closeTime, creatorName, creatorUsername } = contract
|
||||||
|
const { volumeLabel, createdDate, resolvedDate } = contractMetrics(contract)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Col className="gap-2 text-sm text-gray-500 sm:flex-row sm:flex-wrap">
|
||||||
|
<Row className="flex-1 flex-wrap items-center gap-x-4 gap-y-3">
|
||||||
|
<Row className="items-center gap-2">
|
||||||
|
<Avatar
|
||||||
|
username={creatorUsername}
|
||||||
|
avatarUrl={contract.creatorAvatarUrl}
|
||||||
|
size={6}
|
||||||
|
/>
|
||||||
|
<UserLink
|
||||||
|
className="whitespace-nowrap"
|
||||||
|
name={creatorName}
|
||||||
|
username={creatorUsername}
|
||||||
|
/>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
{(!!closeTime || !!resolvedDate) && (
|
||||||
|
<Row className="items-center gap-1">
|
||||||
|
<ClockIcon className="h-5 w-5" />
|
||||||
|
|
||||||
|
{/* <DateTimeTooltip text="Market created:" time={contract.createdTime}>
|
||||||
|
{createdDate}
|
||||||
|
</DateTimeTooltip> */}
|
||||||
|
|
||||||
|
{resolvedDate && contract.resolutionTime ? (
|
||||||
|
<>
|
||||||
|
{/* {' - '} */}
|
||||||
|
<DateTimeTooltip
|
||||||
|
text="Market resolved:"
|
||||||
|
time={contract.resolutionTime}
|
||||||
|
>
|
||||||
|
{resolvedDate}
|
||||||
|
</DateTimeTooltip>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{!resolvedDate && closeTime && (
|
||||||
|
<>
|
||||||
|
{/* {' - '}{' '} */}
|
||||||
|
<EditableCloseDate
|
||||||
|
closeTime={closeTime}
|
||||||
|
contract={contract}
|
||||||
|
isCreator={isCreator ?? false}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Row>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Row className="items-center gap-1">
|
||||||
|
<DatabaseIcon className="h-5 w-5" />
|
||||||
|
|
||||||
|
<div className="whitespace-nowrap">{volumeLabel}</div>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
{!hideShareButtons && (
|
||||||
|
<ContractInfoDialog contract={contract} bets={bets} />
|
||||||
|
)}
|
||||||
|
</Row>
|
||||||
|
</Col>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// String version of the above, to send to the OpenGraph image generator
|
||||||
|
export function contractTextDetails(contract: Contract) {
|
||||||
|
const { closeTime, tags } = contract
|
||||||
|
const { createdDate, resolvedDate, volumeLabel } = contractMetrics(contract)
|
||||||
|
|
||||||
|
const hashtags = tags.map((tag) => `#${tag}`)
|
||||||
|
|
||||||
|
return (
|
||||||
|
`${resolvedDate ? `${createdDate} - ${resolvedDate}` : createdDate}` +
|
||||||
|
(closeTime
|
||||||
|
? ` • ${closeTime > Date.now() ? 'Closes' : 'Closed'} ${dayjs(
|
||||||
|
closeTime
|
||||||
|
).format('MMM D, h:mma')}`
|
||||||
|
: '') +
|
||||||
|
` • ${volumeLabel}` +
|
||||||
|
(hashtags.length > 0 ? ` • ${hashtags.join(' ')}` : '')
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function EditableCloseDate(props: {
|
||||||
|
closeTime: number
|
||||||
|
contract: Contract
|
||||||
|
isCreator: boolean
|
||||||
|
}) {
|
||||||
|
const { closeTime, contract, isCreator } = props
|
||||||
|
|
||||||
|
const [isEditingCloseTime, setIsEditingCloseTime] = useState(false)
|
||||||
|
const [closeDate, setCloseDate] = useState(
|
||||||
|
closeTime && dayjs(closeTime).format('YYYY-MM-DDT23:59')
|
||||||
|
)
|
||||||
|
|
||||||
|
const isSameYear = dayjs(closeTime).isSame(dayjs(), 'year')
|
||||||
|
const isSameDay = dayjs(closeTime).isSame(dayjs(), 'day')
|
||||||
|
|
||||||
|
const onSave = () => {
|
||||||
|
const newCloseTime = dayjs(closeDate).valueOf()
|
||||||
|
if (newCloseTime === closeTime) setIsEditingCloseTime(false)
|
||||||
|
else if (newCloseTime > Date.now()) {
|
||||||
|
const { description } = contract
|
||||||
|
const formattedCloseDate = dayjs(newCloseTime).format('YYYY-MM-DD h:mm a')
|
||||||
|
const newDescription = `${description}\n\nClose date updated to ${formattedCloseDate}`
|
||||||
|
|
||||||
|
updateContract(contract.id, {
|
||||||
|
closeTime: newCloseTime,
|
||||||
|
description: newDescription,
|
||||||
|
})
|
||||||
|
|
||||||
|
setIsEditingCloseTime(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{isEditingCloseTime ? (
|
||||||
|
<div className="form-control mr-1 items-start">
|
||||||
|
<input
|
||||||
|
type="datetime-local"
|
||||||
|
className="input input-bordered"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
onChange={(e) => setCloseDate(e.target.value || '')}
|
||||||
|
min={Date.now()}
|
||||||
|
value={closeDate}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<DateTimeTooltip
|
||||||
|
text={closeTime > Date.now() ? 'Trading ends:' : 'Trading ended:'}
|
||||||
|
time={closeTime}
|
||||||
|
>
|
||||||
|
{isSameYear
|
||||||
|
? dayjs(closeTime).format('MMM D')
|
||||||
|
: dayjs(closeTime).format('MMM D, YYYY')}
|
||||||
|
{isSameDay && <> ({fromNow(closeTime)})</>}
|
||||||
|
</DateTimeTooltip>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isCreator &&
|
||||||
|
(isEditingCloseTime ? (
|
||||||
|
<button className="btn btn-xs" onClick={onSave}>
|
||||||
|
Done
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
className="btn btn-xs btn-ghost"
|
||||||
|
onClick={() => setIsEditingCloseTime(true)}
|
||||||
|
>
|
||||||
|
<PencilIcon className="mr-2 inline h-4 w-4" /> Edit
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
|
@ -8,7 +8,6 @@ import { Linkify } from '../linkify'
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
import {
|
import {
|
||||||
FreeResponseResolutionOrChance,
|
FreeResponseResolutionOrChance,
|
||||||
ContractDetails,
|
|
||||||
BinaryResolutionOrChance,
|
BinaryResolutionOrChance,
|
||||||
} from './contract-card'
|
} from './contract-card'
|
||||||
import { Bet } from '../../../common/bet'
|
import { Bet } from '../../../common/bet'
|
||||||
|
@ -17,6 +16,7 @@ import BetRow from '../bet-row'
|
||||||
import { AnswersGraph } from '../answers/answers-graph'
|
import { AnswersGraph } from '../answers/answers-graph'
|
||||||
import { DPM, FreeResponse, FullContract } from '../../../common/contract'
|
import { DPM, FreeResponse, FullContract } from '../../../common/contract'
|
||||||
import { ContractDescription } from './contract-description'
|
import { ContractDescription } from './contract-description'
|
||||||
|
import { ContractDetails } from './contract-details'
|
||||||
|
|
||||||
export const ContractOverview = (props: {
|
export const ContractOverview = (props: {
|
||||||
contract: Contract
|
contract: Contract
|
||||||
|
|
|
@ -9,6 +9,7 @@ import {
|
||||||
UserIcon,
|
UserIcon,
|
||||||
UsersIcon,
|
UsersIcon,
|
||||||
XIcon,
|
XIcon,
|
||||||
|
SparklesIcon,
|
||||||
} from '@heroicons/react/solid'
|
} from '@heroicons/react/solid'
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
import Textarea from 'react-expanding-textarea'
|
import Textarea from 'react-expanding-textarea'
|
||||||
|
@ -46,6 +47,8 @@ import { useSaveSeenContract } from '../../hooks/use-seen-contracts'
|
||||||
import { User } from '../../../common/user'
|
import { User } from '../../../common/user'
|
||||||
import { Modal } from '../layout/modal'
|
import { Modal } from '../layout/modal'
|
||||||
import { trackClick } from '../../lib/firebase/tracking'
|
import { trackClick } from '../../lib/firebase/tracking'
|
||||||
|
import { DAY_MS } from '../../../common/util/time'
|
||||||
|
import NewContractBadge from '../new-contract-badge'
|
||||||
|
|
||||||
export function FeedItems(props: {
|
export function FeedItems(props: {
|
||||||
contract: Contract
|
contract: Contract
|
||||||
|
@ -307,19 +310,17 @@ export function FeedQuestion(props: {
|
||||||
contractPath?: string
|
contractPath?: string
|
||||||
}) {
|
}) {
|
||||||
const { contract, showDescription } = props
|
const { contract, showDescription } = props
|
||||||
const { creatorName, creatorUsername, question, resolution, outcomeType } =
|
const {
|
||||||
contract
|
creatorName,
|
||||||
|
creatorUsername,
|
||||||
|
question,
|
||||||
|
outcomeType,
|
||||||
|
volume,
|
||||||
|
createdTime,
|
||||||
|
} = contract
|
||||||
const { volumeLabel } = contractMetrics(contract)
|
const { volumeLabel } = contractMetrics(contract)
|
||||||
const isBinary = outcomeType === 'BINARY'
|
const isBinary = outcomeType === 'BINARY'
|
||||||
|
const isNew = createdTime > Date.now() - DAY_MS
|
||||||
// const closeMessage =
|
|
||||||
// contract.isResolved || !contract.closeTime ? null : (
|
|
||||||
// <>
|
|
||||||
// <span className="mx-2">•</span>
|
|
||||||
// {contract.closeTime > Date.now() ? 'Closes' : 'Closed'}
|
|
||||||
// <RelativeTimestamp time={contract.closeTime || 0} />
|
|
||||||
// </>
|
|
||||||
// )
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -336,10 +337,15 @@ export function FeedQuestion(props: {
|
||||||
/>{' '}
|
/>{' '}
|
||||||
asked
|
asked
|
||||||
{/* Currently hidden on mobile; ideally we'd fit this in somewhere. */}
|
{/* Currently hidden on mobile; ideally we'd fit this in somewhere. */}
|
||||||
<span className="float-right hidden text-gray-400 sm:inline">
|
<div className="relative -top-2 float-right ">
|
||||||
{volumeLabel}
|
{isNew || volume === 0 ? (
|
||||||
{/* {closeMessage} */}
|
<NewContractBadge />
|
||||||
</span>
|
) : (
|
||||||
|
<span className="hidden text-gray-400 sm:inline">
|
||||||
|
{volumeLabel}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Col className="items-start justify-between gap-2 sm:flex-row sm:gap-4">
|
<Col className="items-start justify-between gap-2 sm:flex-row sm:gap-4">
|
||||||
<Col>
|
<Col>
|
||||||
|
|
|
@ -158,6 +158,12 @@ export default function Sidebar() {
|
||||||
buttonContent={<MoreButton />}
|
buttonContent={<MoreButton />}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{user && (
|
||||||
|
<Link href={'/create'}>
|
||||||
|
<button className="btn btn-primary btn-md mt-4">Create Market</button>
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
</nav>
|
</nav>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
9
web/components/new-contract-badge.tsx
Normal file
9
web/components/new-contract-badge.tsx
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import { SparklesIcon } from '@heroicons/react/solid'
|
||||||
|
|
||||||
|
export default function NewContractBadge() {
|
||||||
|
return (
|
||||||
|
<span className="inline-flex items-center gap-1 rounded-full bg-blue-100 px-3 py-0.5 text-sm font-medium text-blue-800">
|
||||||
|
<SparklesIcon className="h-4 w-4" aria-hidden="true" /> New
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,3 +1,4 @@
|
||||||
|
import clsx from 'clsx'
|
||||||
import { Answer } from '../../common/answer'
|
import { Answer } from '../../common/answer'
|
||||||
import { getProbability } from '../../common/calculate'
|
import { getProbability } from '../../common/calculate'
|
||||||
import {
|
import {
|
||||||
|
@ -59,8 +60,9 @@ export function FreeResponseOutcomeLabel(props: {
|
||||||
contract: FreeResponseContract
|
contract: FreeResponseContract
|
||||||
resolution: string | 'CANCEL' | 'MKT'
|
resolution: string | 'CANCEL' | 'MKT'
|
||||||
truncate: 'short' | 'long' | 'none'
|
truncate: 'short' | 'long' | 'none'
|
||||||
|
answerClassName?: string
|
||||||
}) {
|
}) {
|
||||||
const { contract, resolution, truncate } = props
|
const { contract, resolution, truncate, answerClassName } = props
|
||||||
|
|
||||||
if (resolution === 'CANCEL') return <CancelLabel />
|
if (resolution === 'CANCEL') return <CancelLabel />
|
||||||
if (resolution === 'MKT') return <MultiLabel />
|
if (resolution === 'MKT') return <MultiLabel />
|
||||||
|
@ -68,7 +70,13 @@ export function FreeResponseOutcomeLabel(props: {
|
||||||
const { answers } = contract
|
const { answers } = contract
|
||||||
const chosen = answers?.find((answer) => answer.id === resolution)
|
const chosen = answers?.find((answer) => answer.id === resolution)
|
||||||
if (!chosen) return <AnswerNumberLabel number={resolution} />
|
if (!chosen) return <AnswerNumberLabel number={resolution} />
|
||||||
return <AnswerLabel answer={chosen} truncate={truncate} />
|
return (
|
||||||
|
<AnswerLabel
|
||||||
|
answer={chosen}
|
||||||
|
truncate={truncate}
|
||||||
|
className={answerClassName}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function YesLabel() {
|
export function YesLabel() {
|
||||||
|
@ -103,8 +111,9 @@ export function AnswerNumberLabel(props: { number: string }) {
|
||||||
export function AnswerLabel(props: {
|
export function AnswerLabel(props: {
|
||||||
answer: Answer
|
answer: Answer
|
||||||
truncate: 'short' | 'long' | 'none'
|
truncate: 'short' | 'long' | 'none'
|
||||||
|
className?: string
|
||||||
}) {
|
}) {
|
||||||
const { answer, truncate } = props
|
const { answer, truncate, className } = props
|
||||||
const { text } = answer
|
const { text } = answer
|
||||||
|
|
||||||
let truncated = text
|
let truncated = text
|
||||||
|
@ -114,5 +123,5 @@ export function AnswerLabel(props: {
|
||||||
truncated = text.slice(0, 75) + '...'
|
truncated = text.slice(0, 75) + '...'
|
||||||
}
|
}
|
||||||
|
|
||||||
return <span className="text-primary">{truncated}</span>
|
return <span className={className}>{truncated}</span>
|
||||||
}
|
}
|
||||||
|
|
64
web/components/sell-button.tsx
Normal file
64
web/components/sell-button.tsx
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
import { Binary, CPMM, DPM, FullContract } from '../../common/contract'
|
||||||
|
import { User } from '../../common/user'
|
||||||
|
import { useUserContractBets } from '../hooks/use-user-bets'
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { useSaveShares } from './use-save-shares'
|
||||||
|
import { Col } from './layout/col'
|
||||||
|
import clsx from 'clsx'
|
||||||
|
import { SellSharesModal } from './sell-modal'
|
||||||
|
|
||||||
|
export function SellButton(props: {
|
||||||
|
contract: FullContract<DPM | CPMM, Binary>
|
||||||
|
user: User | null | undefined
|
||||||
|
}) {
|
||||||
|
const { contract, user } = props
|
||||||
|
|
||||||
|
const userBets = useUserContractBets(user?.id, contract.id)
|
||||||
|
const [showSellModal, setShowSellModal] = useState(false)
|
||||||
|
|
||||||
|
const { mechanism } = contract
|
||||||
|
const { yesFloorShares, noFloorShares, yesShares, noShares } = useSaveShares(
|
||||||
|
contract,
|
||||||
|
userBets
|
||||||
|
)
|
||||||
|
const floorShares = yesFloorShares || noFloorShares
|
||||||
|
const sharesOutcome = yesFloorShares
|
||||||
|
? 'YES'
|
||||||
|
: noFloorShares
|
||||||
|
? 'NO'
|
||||||
|
: undefined
|
||||||
|
|
||||||
|
if (sharesOutcome && user && mechanism === 'cpmm-1') {
|
||||||
|
return (
|
||||||
|
<Col className={'items-center'}>
|
||||||
|
<button
|
||||||
|
className={clsx(
|
||||||
|
'btn-sm w-24 gap-1',
|
||||||
|
// from the yes-no-selector:
|
||||||
|
'flex inline-flex flex-row items-center justify-center rounded-3xl border-2 p-2',
|
||||||
|
sharesOutcome === 'NO'
|
||||||
|
? 'hover:bg-primary-focus border-primary hover:border-primary-focus text-primary hover:text-white'
|
||||||
|
: 'border-red-400 text-red-500 hover:border-red-500 hover:bg-red-500 hover:text-white'
|
||||||
|
)}
|
||||||
|
onClick={() => setShowSellModal(true)}
|
||||||
|
>
|
||||||
|
{'Sell ' + sharesOutcome}
|
||||||
|
</button>
|
||||||
|
<div className={'mt-1 w-24 text-center text-sm text-gray-500'}>
|
||||||
|
{'(' + floorShares + ' shares)'}
|
||||||
|
</div>
|
||||||
|
{showSellModal && (
|
||||||
|
<SellSharesModal
|
||||||
|
contract={contract as FullContract<CPMM, Binary>}
|
||||||
|
user={user}
|
||||||
|
userBets={userBets ?? []}
|
||||||
|
shares={yesShares || noShares}
|
||||||
|
sharesOutcome={sharesOutcome}
|
||||||
|
setOpen={setShowSellModal}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Col>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return <div />
|
||||||
|
}
|
47
web/components/sell-modal.tsx
Normal file
47
web/components/sell-modal.tsx
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
import { Binary, CPMM, FullContract } from '../../common/contract'
|
||||||
|
import { Bet } from '../../common/bet'
|
||||||
|
import { User } from '../../common/user'
|
||||||
|
import { Modal } from './layout/modal'
|
||||||
|
import { Col } from './layout/col'
|
||||||
|
import { Title } from './title'
|
||||||
|
import { formatWithCommas } from '../../common/util/format'
|
||||||
|
import { OutcomeLabel } from './outcome-label'
|
||||||
|
import { SellPanel } from './bet-panel'
|
||||||
|
|
||||||
|
export function SellSharesModal(props: {
|
||||||
|
contract: FullContract<CPMM, Binary>
|
||||||
|
userBets: Bet[]
|
||||||
|
shares: number
|
||||||
|
sharesOutcome: 'YES' | 'NO'
|
||||||
|
user: User
|
||||||
|
setOpen: (open: boolean) => void
|
||||||
|
}) {
|
||||||
|
const { contract, shares, sharesOutcome, userBets, user, setOpen } = props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal open={true} setOpen={setOpen}>
|
||||||
|
<Col className="rounded-md bg-white px-8 py-6">
|
||||||
|
<Title className="!mt-0" text={'Sell shares'} />
|
||||||
|
|
||||||
|
<div className="mb-6">
|
||||||
|
You have {formatWithCommas(Math.floor(shares))}{' '}
|
||||||
|
<OutcomeLabel
|
||||||
|
outcome={sharesOutcome}
|
||||||
|
contract={contract}
|
||||||
|
truncate={'short'}
|
||||||
|
/>{' '}
|
||||||
|
shares
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<SellPanel
|
||||||
|
contract={contract}
|
||||||
|
shares={shares}
|
||||||
|
sharesOutcome={sharesOutcome}
|
||||||
|
user={user}
|
||||||
|
userBets={userBets ?? []}
|
||||||
|
onSellSuccess={() => setOpen(false)}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
77
web/components/sell-row.tsx
Normal file
77
web/components/sell-row.tsx
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
import { Binary, CPMM, DPM, FullContract } from '../../common/contract'
|
||||||
|
import { User } from '../../common/user'
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { Col } from './layout/col'
|
||||||
|
import { Row } from './layout/row'
|
||||||
|
import { formatWithCommas } from '../../common/util/format'
|
||||||
|
import { OutcomeLabel } from './outcome-label'
|
||||||
|
import { useUserContractBets } from '../hooks/use-user-bets'
|
||||||
|
import { useSaveShares } from './use-save-shares'
|
||||||
|
import { SellSharesModal } from './sell-modal'
|
||||||
|
|
||||||
|
export function SellRow(props: {
|
||||||
|
contract: FullContract<DPM | CPMM, Binary>
|
||||||
|
user: User | null | undefined
|
||||||
|
className?: string
|
||||||
|
}) {
|
||||||
|
const { className, contract, user } = props
|
||||||
|
|
||||||
|
const userBets = useUserContractBets(user?.id, contract.id)
|
||||||
|
const [showSellModal, setShowSellModal] = useState(false)
|
||||||
|
|
||||||
|
const { mechanism } = contract
|
||||||
|
const { yesFloorShares, noFloorShares, yesShares, noShares } = useSaveShares(
|
||||||
|
contract,
|
||||||
|
userBets
|
||||||
|
)
|
||||||
|
const floorShares = yesFloorShares || noFloorShares
|
||||||
|
const sharesOutcome = yesFloorShares
|
||||||
|
? 'YES'
|
||||||
|
: noFloorShares
|
||||||
|
? 'NO'
|
||||||
|
: undefined
|
||||||
|
|
||||||
|
if (sharesOutcome && user && mechanism === 'cpmm-1') {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Col className={className}>
|
||||||
|
<Row className="items-center justify-between gap-2 ">
|
||||||
|
<div>
|
||||||
|
You have {formatWithCommas(floorShares)}{' '}
|
||||||
|
<OutcomeLabel
|
||||||
|
outcome={sharesOutcome}
|
||||||
|
contract={contract}
|
||||||
|
truncate={'short'}
|
||||||
|
/>{' '}
|
||||||
|
shares
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
className="btn btn-sm"
|
||||||
|
style={{
|
||||||
|
backgroundColor: 'white',
|
||||||
|
border: '2px solid',
|
||||||
|
color: '#3D4451',
|
||||||
|
}}
|
||||||
|
onClick={() => setShowSellModal(true)}
|
||||||
|
>
|
||||||
|
Sell
|
||||||
|
</button>
|
||||||
|
</Row>
|
||||||
|
</Col>
|
||||||
|
{showSellModal && (
|
||||||
|
<SellSharesModal
|
||||||
|
contract={contract as FullContract<CPMM, Binary>}
|
||||||
|
user={user}
|
||||||
|
userBets={userBets ?? []}
|
||||||
|
shares={yesShares || noShares}
|
||||||
|
sharesOutcome={sharesOutcome}
|
||||||
|
setOpen={setShowSellModal}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div />
|
||||||
|
}
|
59
web/components/use-save-shares.ts
Normal file
59
web/components/use-save-shares.ts
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
import { Binary, CPMM, DPM, FullContract } from '../../common/contract'
|
||||||
|
import { Bet } from '../../common/bet'
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import _ from 'lodash'
|
||||||
|
|
||||||
|
export const useSaveShares = (
|
||||||
|
contract: FullContract<CPMM | DPM, Binary>,
|
||||||
|
userBets: Bet[] | undefined
|
||||||
|
) => {
|
||||||
|
const [savedShares, setSavedShares] = useState<
|
||||||
|
| {
|
||||||
|
yesShares: number
|
||||||
|
noShares: number
|
||||||
|
yesFloorShares: number
|
||||||
|
noFloorShares: number
|
||||||
|
}
|
||||||
|
| undefined
|
||||||
|
>()
|
||||||
|
|
||||||
|
const [yesBets, noBets] = _.partition(
|
||||||
|
userBets ?? [],
|
||||||
|
(bet) => bet.outcome === 'YES'
|
||||||
|
)
|
||||||
|
const [yesShares, noShares] = [
|
||||||
|
_.sumBy(yesBets, (bet) => bet.shares),
|
||||||
|
_.sumBy(noBets, (bet) => bet.shares),
|
||||||
|
]
|
||||||
|
|
||||||
|
const [yesFloorShares, noFloorShares] = [
|
||||||
|
Math.floor(yesShares),
|
||||||
|
Math.floor(noShares),
|
||||||
|
]
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Save yes and no shares to local storage.
|
||||||
|
const savedShares = localStorage.getItem(`${contract.id}-shares`)
|
||||||
|
if (!userBets && savedShares) {
|
||||||
|
setSavedShares(JSON.parse(savedShares))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userBets) {
|
||||||
|
const updatedShares = { yesShares, noShares }
|
||||||
|
localStorage.setItem(
|
||||||
|
`${contract.id}-shares`,
|
||||||
|
JSON.stringify(updatedShares)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}, [contract.id, userBets, noShares, yesShares])
|
||||||
|
|
||||||
|
if (userBets) return { yesShares, noShares, yesFloorShares, noFloorShares }
|
||||||
|
return (
|
||||||
|
savedShares ?? {
|
||||||
|
yesShares: 0,
|
||||||
|
noShares: 0,
|
||||||
|
yesFloorShares: 0,
|
||||||
|
noFloorShares: 0,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
|
@ -87,7 +87,12 @@ export function UserPage(props: { user: User; currentUser?: User }) {
|
||||||
|
|
||||||
<Col className="sm:flex-row sm:gap-4">
|
<Col className="sm:flex-row sm:gap-4">
|
||||||
{user.website && (
|
{user.website && (
|
||||||
<SiteLink href={user.website}>
|
<SiteLink
|
||||||
|
href={
|
||||||
|
'https://' +
|
||||||
|
user.website.replace('http://', '').replace('https://', '')
|
||||||
|
}
|
||||||
|
>
|
||||||
<Row className="items-center gap-1">
|
<Row className="items-center gap-1">
|
||||||
<LinkIcon className="h-4 w-4" />
|
<LinkIcon className="h-4 w-4" />
|
||||||
<span className="text-sm text-gray-500">{user.website}</span>
|
<span className="text-sm text-gray-500">{user.website}</span>
|
||||||
|
@ -96,7 +101,13 @@ export function UserPage(props: { user: User; currentUser?: User }) {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{user.twitterHandle && (
|
{user.twitterHandle && (
|
||||||
<SiteLink href={`https://twitter.com/${user.twitterHandle}`}>
|
<SiteLink
|
||||||
|
href={`https://twitter.com/${user.twitterHandle
|
||||||
|
.replace('https://www.twitter.com/', '')
|
||||||
|
.replace('https://twitter.com/', '')
|
||||||
|
.replace('www.twitter.com/', '')
|
||||||
|
.replace('twitter.com/', '')}`}
|
||||||
|
>
|
||||||
<Row className="items-center gap-1">
|
<Row className="items-center gap-1">
|
||||||
<img
|
<img
|
||||||
src="/twitter-logo.svg"
|
src="/twitter-logo.svg"
|
||||||
|
|
|
@ -9,40 +9,57 @@ export function YesNoSelector(props: {
|
||||||
onSelect: (selected: 'YES' | 'NO') => void
|
onSelect: (selected: 'YES' | 'NO') => void
|
||||||
className?: string
|
className?: string
|
||||||
btnClassName?: string
|
btnClassName?: string
|
||||||
|
replaceYesButton?: React.ReactNode
|
||||||
|
replaceNoButton?: React.ReactNode
|
||||||
}) {
|
}) {
|
||||||
const { selected, onSelect, className, btnClassName } = props
|
const {
|
||||||
|
selected,
|
||||||
|
onSelect,
|
||||||
|
className,
|
||||||
|
btnClassName,
|
||||||
|
replaceNoButton,
|
||||||
|
replaceYesButton,
|
||||||
|
} = props
|
||||||
|
|
||||||
const commonClassNames =
|
const commonClassNames =
|
||||||
'inline-flex flex-1 items-center justify-center rounded-3xl border-2 p-2'
|
'inline-flex flex-1 items-center justify-center rounded-3xl border-2 p-2'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Row className={clsx('space-x-3', className)}>
|
<Row className={clsx('space-x-3', className)}>
|
||||||
<button
|
{replaceYesButton ? (
|
||||||
className={clsx(
|
replaceYesButton
|
||||||
commonClassNames,
|
) : (
|
||||||
'hover:bg-primary-focus border-primary hover:border-primary-focus hover:text-white',
|
<button
|
||||||
selected == 'YES'
|
className={clsx(
|
||||||
? 'bg-primary text-white'
|
commonClassNames,
|
||||||
: 'text-primary bg-transparent',
|
'hover:bg-primary-focus border-primary hover:border-primary-focus hover:text-white',
|
||||||
btnClassName
|
selected == 'YES'
|
||||||
)}
|
? 'bg-primary text-white'
|
||||||
onClick={() => onSelect('YES')}
|
: 'text-primary bg-transparent',
|
||||||
>
|
btnClassName
|
||||||
Bet YES
|
)}
|
||||||
</button>
|
onClick={() => onSelect('YES')}
|
||||||
<button
|
>
|
||||||
className={clsx(
|
Bet YES
|
||||||
commonClassNames,
|
</button>
|
||||||
'border-red-400 hover:border-red-500 hover:bg-red-500 hover:text-white',
|
)}
|
||||||
selected == 'NO'
|
{replaceNoButton ? (
|
||||||
? 'bg-red-400 text-white'
|
replaceNoButton
|
||||||
: 'bg-transparent text-red-400',
|
) : (
|
||||||
btnClassName
|
<button
|
||||||
)}
|
className={clsx(
|
||||||
onClick={() => onSelect('NO')}
|
commonClassNames,
|
||||||
>
|
'border-red-400 hover:border-red-500 hover:bg-red-500 hover:text-white',
|
||||||
Bet NO
|
selected == 'NO'
|
||||||
</button>
|
? 'bg-red-400 text-white'
|
||||||
|
: 'bg-transparent text-red-400',
|
||||||
|
btnClassName
|
||||||
|
)}
|
||||||
|
onClick={() => onSelect('NO')}
|
||||||
|
>
|
||||||
|
Bet NO
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</Row>
|
</Row>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,14 @@ import { logInterpolation } from '../../common/util/math'
|
||||||
import { getRecommendedContracts } from '../../common/recommended-contracts'
|
import { getRecommendedContracts } from '../../common/recommended-contracts'
|
||||||
import { useSeenContracts } from './use-seen-contracts'
|
import { useSeenContracts } from './use-seen-contracts'
|
||||||
import { useGetUserBetContractIds, useUserBetContracts } from './use-user-bets'
|
import { useGetUserBetContractIds, useUserBetContracts } from './use-user-bets'
|
||||||
|
import { DAY_MS } from '../../common/util/time'
|
||||||
|
import {
|
||||||
|
getProbability,
|
||||||
|
getOutcomeProbability,
|
||||||
|
getTopAnswer,
|
||||||
|
} from '../../common/calculate'
|
||||||
|
import { useTimeSinceFirstRender } from './use-time-since-first-render'
|
||||||
|
import { trackLatency } from '../lib/firebase/tracking'
|
||||||
|
|
||||||
const MAX_FEED_CONTRACTS = 75
|
const MAX_FEED_CONTRACTS = 75
|
||||||
|
|
||||||
|
@ -29,8 +37,15 @@ export const useAlgoFeed = (
|
||||||
|
|
||||||
const [algoFeed, setAlgoFeed] = useState<Contract[]>([])
|
const [algoFeed, setAlgoFeed] = useState<Contract[]>([])
|
||||||
|
|
||||||
|
const getTime = useTimeSinceFirstRender()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (initialContracts && initialBets && initialComments) {
|
if (
|
||||||
|
initialContracts &&
|
||||||
|
initialBets &&
|
||||||
|
initialComments &&
|
||||||
|
yourBetContractIds
|
||||||
|
) {
|
||||||
const eligibleContracts = initialContracts.filter(
|
const eligibleContracts = initialContracts.filter(
|
||||||
(c) => !c.isResolved && (c.closeTime ?? Infinity) > Date.now()
|
(c) => !c.isResolved && (c.closeTime ?? Infinity) > Date.now()
|
||||||
)
|
)
|
||||||
|
@ -42,6 +57,7 @@ export const useAlgoFeed = (
|
||||||
seenContracts
|
seenContracts
|
||||||
)
|
)
|
||||||
setAlgoFeed(contracts)
|
setAlgoFeed(contracts)
|
||||||
|
trackLatency('feed', getTime())
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
initialBets,
|
initialBets,
|
||||||
|
@ -49,6 +65,7 @@ export const useAlgoFeed = (
|
||||||
initialContracts,
|
initialContracts,
|
||||||
seenContracts,
|
seenContracts,
|
||||||
yourBetContractIds,
|
yourBetContractIds,
|
||||||
|
getTime,
|
||||||
])
|
])
|
||||||
|
|
||||||
return algoFeed
|
return algoFeed
|
||||||
|
@ -120,11 +137,13 @@ function getContractsActivityScores(
|
||||||
)
|
)
|
||||||
|
|
||||||
const scoredContracts = contracts.map((contract) => {
|
const scoredContracts = contracts.map((contract) => {
|
||||||
|
const { outcomeType } = contract
|
||||||
|
|
||||||
const seenTime = seenContracts[contract.id]
|
const seenTime = seenContracts[contract.id]
|
||||||
const lastCommentTime = contractMostRecentComment[contract.id]?.createdTime
|
const lastCommentTime = contractMostRecentComment[contract.id]?.createdTime
|
||||||
const hasNewComments =
|
const hasNewComments =
|
||||||
!seenTime || (lastCommentTime && lastCommentTime > seenTime)
|
!seenTime || (lastCommentTime && lastCommentTime > seenTime)
|
||||||
const newCommentScore = hasNewComments ? 1 : 0.75
|
const newCommentScore = hasNewComments ? 1 : 0.5
|
||||||
|
|
||||||
const commentCount = contractComments[contract.id]?.length ?? 0
|
const commentCount = contractComments[contract.id]?.length ?? 0
|
||||||
const betCount = contractBets[contract.id]?.length ?? 0
|
const betCount = contractBets[contract.id]?.length ?? 0
|
||||||
|
@ -132,25 +151,39 @@ function getContractsActivityScores(
|
||||||
const activityCountScore =
|
const activityCountScore =
|
||||||
0.5 + 0.5 * logInterpolation(0, 200, activtyCount)
|
0.5 + 0.5 * logInterpolation(0, 200, activtyCount)
|
||||||
|
|
||||||
const lastBetTime = contractMostRecentBet[contract.id]?.createdTime
|
const lastBetTime =
|
||||||
const timeSinceLastBet = !lastBetTime
|
contractMostRecentBet[contract.id]?.createdTime ?? contract.createdTime
|
||||||
? contract.createdTime
|
const timeSinceLastBet = Date.now() - lastBetTime
|
||||||
: Date.now() - lastBetTime
|
const daysAgo = timeSinceLastBet / DAY_MS
|
||||||
const daysAgo = timeSinceLastBet / oneDayMs
|
|
||||||
const timeAgoScore = 1 - logInterpolation(0, 3, daysAgo)
|
const timeAgoScore = 1 - logInterpolation(0, 3, daysAgo)
|
||||||
|
|
||||||
const score = newCommentScore * activityCountScore * timeAgoScore
|
let prob = 0.5
|
||||||
|
if (outcomeType === 'BINARY') {
|
||||||
|
prob = getProbability(contract)
|
||||||
|
} else if (outcomeType === 'FREE_RESPONSE') {
|
||||||
|
const topAnswer = getTopAnswer(contract)
|
||||||
|
if (topAnswer)
|
||||||
|
prob = Math.max(0.5, getOutcomeProbability(contract, topAnswer.id))
|
||||||
|
}
|
||||||
|
const frac = 1 - Math.abs(prob - 0.5) ** 2 / 0.25
|
||||||
|
const probScore = 0.5 + frac * 0.5
|
||||||
|
|
||||||
|
const score =
|
||||||
|
newCommentScore * activityCountScore * timeAgoScore * probScore
|
||||||
|
|
||||||
// Map score to [0.5, 1] since no recent activty is not a deal breaker.
|
// Map score to [0.5, 1] since no recent activty is not a deal breaker.
|
||||||
const mappedScore = 0.5 + score / 2
|
const mappedScore = 0.5 + score / 2
|
||||||
return [contract.id, mappedScore] as [string, number]
|
const newMappedScore = 0.75 + score / 4
|
||||||
|
|
||||||
|
const isNew = Date.now() < contract.createdTime + DAY_MS
|
||||||
|
const activityScore = isNew ? newMappedScore : mappedScore
|
||||||
|
|
||||||
|
return [contract.id, activityScore] as [string, number]
|
||||||
})
|
})
|
||||||
|
|
||||||
return _.fromPairs(scoredContracts)
|
return _.fromPairs(scoredContracts)
|
||||||
}
|
}
|
||||||
|
|
||||||
const oneDayMs = 24 * 60 * 60 * 1000
|
|
||||||
|
|
||||||
function getSeenContractsScore(
|
function getSeenContractsScore(
|
||||||
contract: Contract,
|
contract: Contract,
|
||||||
seenContracts: { [contractId: string]: number }
|
seenContracts: { [contractId: string]: number }
|
||||||
|
@ -160,7 +193,7 @@ function getSeenContractsScore(
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
const daysAgo = (Date.now() - lastSeen) / oneDayMs
|
const daysAgo = (Date.now() - lastSeen) / DAY_MS
|
||||||
|
|
||||||
if (daysAgo < 0.5) {
|
if (daysAgo < 0.5) {
|
||||||
const frac = logInterpolation(0, 0.5, daysAgo)
|
const frac = logInterpolation(0, 0.5, daysAgo)
|
||||||
|
|
|
@ -6,6 +6,7 @@ import {
|
||||||
listenForContracts,
|
listenForContracts,
|
||||||
listenForHotContracts,
|
listenForHotContracts,
|
||||||
listenForInactiveContracts,
|
listenForInactiveContracts,
|
||||||
|
listenForNewContracts,
|
||||||
} from '../lib/firebase/contracts'
|
} from '../lib/firebase/contracts'
|
||||||
import { listenForTaggedContracts } from '../lib/firebase/folds'
|
import { listenForTaggedContracts } from '../lib/firebase/folds'
|
||||||
|
|
||||||
|
@ -20,13 +21,22 @@ export const useContracts = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useActiveContracts = () => {
|
export const useActiveContracts = () => {
|
||||||
const [contracts, setContracts] = useState<Contract[] | undefined>()
|
const [activeContracts, setActiveContracts] = useState<
|
||||||
|
Contract[] | undefined
|
||||||
|
>()
|
||||||
|
const [newContracts, setNewContracts] = useState<Contract[] | undefined>()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return listenForActiveContracts(setContracts)
|
return listenForActiveContracts(setActiveContracts)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return contracts
|
useEffect(() => {
|
||||||
|
return listenForNewContracts(setNewContracts)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
if (!activeContracts || !newContracts) return undefined
|
||||||
|
|
||||||
|
return [...activeContracts, ...newContracts]
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useInactiveContracts = () => {
|
export const useInactiveContracts = () => {
|
||||||
|
|
13
web/hooks/use-time-since-first-render.ts
Normal file
13
web/hooks/use-time-since-first-render.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import { useCallback, useEffect, useRef } from 'react'
|
||||||
|
|
||||||
|
export function useTimeSinceFirstRender() {
|
||||||
|
const startTimeRef = useRef(0)
|
||||||
|
useEffect(() => {
|
||||||
|
startTimeRef.current = Date.now()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return useCallback(() => {
|
||||||
|
if (!startTimeRef.current) return 0
|
||||||
|
return Date.now() - startTimeRef.current
|
||||||
|
}, [])
|
||||||
|
}
|
|
@ -54,13 +54,15 @@ export const useUserBetContracts = (userId: string | undefined) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useGetUserBetContractIds = (userId: string | undefined) => {
|
export const useGetUserBetContractIds = (userId: string | undefined) => {
|
||||||
const [contractIds, setContractIds] = useState<string[]>([])
|
const [contractIds, setContractIds] = useState<string[] | undefined>()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const key = `user-bet-contractIds-${userId}`
|
if (userId) {
|
||||||
const userBetContractJson = localStorage.getItem(key)
|
const key = `user-bet-contractIds-${userId}`
|
||||||
if (userBetContractJson) {
|
const userBetContractJson = localStorage.getItem(key)
|
||||||
setContractIds(JSON.parse(userBetContractJson))
|
if (userBetContractJson) {
|
||||||
|
setContractIds(JSON.parse(userBetContractJson))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [userId])
|
}, [userId])
|
||||||
|
|
||||||
|
|
|
@ -22,6 +22,7 @@ import { getDpmProbability } from '../../../common/calculate-dpm'
|
||||||
import { createRNG, shuffle } from '../../../common/util/random'
|
import { createRNG, shuffle } from '../../../common/util/random'
|
||||||
import { getCpmmProbability } from '../../../common/calculate-cpmm'
|
import { getCpmmProbability } from '../../../common/calculate-cpmm'
|
||||||
import { formatMoney, formatPercent } from '../../../common/util/format'
|
import { formatMoney, formatPercent } from '../../../common/util/format'
|
||||||
|
import { DAY_MS } from '../../../common/util/time'
|
||||||
export type { Contract }
|
export type { Contract }
|
||||||
|
|
||||||
export function contractPath(contract: Contract) {
|
export function contractPath(contract: Contract) {
|
||||||
|
@ -162,6 +163,19 @@ export function listenForInactiveContracts(
|
||||||
return listenForValues<Contract>(inactiveContractsQuery, setContracts)
|
return listenForValues<Contract>(inactiveContractsQuery, setContracts)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const newContractsQuery = query(
|
||||||
|
contractCollection,
|
||||||
|
where('isResolved', '==', false),
|
||||||
|
where('volume7Days', '==', 0),
|
||||||
|
where('createdTime', '>', Date.now() - 7 * DAY_MS)
|
||||||
|
)
|
||||||
|
|
||||||
|
export function listenForNewContracts(
|
||||||
|
setContracts: (contracts: Contract[]) => void
|
||||||
|
) {
|
||||||
|
return listenForValues<Contract>(newContractsQuery, setContracts)
|
||||||
|
}
|
||||||
|
|
||||||
export function listenForContract(
|
export function listenForContract(
|
||||||
contractId: string,
|
contractId: string,
|
||||||
setContract: (contract: Contract | null) => void
|
setContract: (contract: Contract | null) => void
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { doc, collection, setDoc } from 'firebase/firestore'
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
|
|
||||||
import { db } from './init'
|
import { db } from './init'
|
||||||
import { ClickEvent, View } from '../../../common/tracking'
|
import { ClickEvent, LatencyEvent, View } from '../../../common/tracking'
|
||||||
import { listenForLogin, User } from './users'
|
import { listenForLogin, User } from './users'
|
||||||
|
|
||||||
let user: User | null = null
|
let user: User | null = null
|
||||||
|
@ -34,3 +34,19 @@ export async function trackClick(contractId: string) {
|
||||||
|
|
||||||
return await setDoc(ref, clickEvent)
|
return await setDoc(ref, clickEvent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function trackLatency(
|
||||||
|
type: 'feed' | 'portfolio',
|
||||||
|
latency: number
|
||||||
|
) {
|
||||||
|
if (!user) return
|
||||||
|
const ref = doc(collection(db, 'private-users', user.id, 'latency'))
|
||||||
|
|
||||||
|
const latencyEvent: LatencyEvent = {
|
||||||
|
type,
|
||||||
|
latency,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
return await setDoc(ref, latencyEvent)
|
||||||
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
"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\"",
|
"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\" # see https://github.com/vercel/next.js/discussions/33634",
|
||||||
"dev:dev": "yarn devdev",
|
"dev:dev": "yarn devdev",
|
||||||
"dev:the": "NEXT_PUBLIC_FIREBASE_ENV=THEOREMONE concurrently -n NEXT,TS -c magenta,cyan \"FIREBASE_ENV=THEOREMONE next dev -p 3000\" \"FIREBASE_ENV=THEOREMONE yarn ts --watch\"",
|
"dev:the": "NEXT_PUBLIC_FIREBASE_ENV=THEOREMONE concurrently -n NEXT,TS -c magenta,cyan \"FIREBASE_ENV=THEOREMONE next dev -p 3000\" \"FIREBASE_ENV=THEOREMONE yarn ts --watch\"",
|
||||||
"ts": "tsc --noEmit --incremental --preserveWatchOutput --pretty",
|
"ts": "tsc --noEmit --incremental --preserveWatchOutput --pretty",
|
||||||
|
|
|
@ -19,7 +19,6 @@ export default function Custom404() {
|
||||||
src="https://discord.com/widget?id=915138780216823849&theme=dark"
|
src="https://discord.com/widget?id=915138780216823849&theme=dark"
|
||||||
width="350"
|
width="350"
|
||||||
height="500"
|
height="500"
|
||||||
allowTransparency={true}
|
|
||||||
frameBorder="0"
|
frameBorder="0"
|
||||||
sandbox="allow-popups allow-popups-to-escape-sandbox allow-same-origin allow-scripts"
|
sandbox="allow-popups allow-popups-to-escape-sandbox allow-same-origin allow-scripts"
|
||||||
></iframe>
|
></iframe>
|
||||||
|
|
|
@ -18,7 +18,6 @@ import {
|
||||||
} from '../../lib/firebase/contracts'
|
} from '../../lib/firebase/contracts'
|
||||||
import { SEO } from '../../components/SEO'
|
import { SEO } from '../../components/SEO'
|
||||||
import { Page } from '../../components/page'
|
import { Page } from '../../components/page'
|
||||||
import { contractTextDetails } from '../../components/contract/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'
|
||||||
|
@ -33,6 +32,7 @@ import { useUserById } from '../../hooks/use-users'
|
||||||
import { ContractTabs } from '../../components/contract/contract-tabs'
|
import { ContractTabs } from '../../components/contract/contract-tabs'
|
||||||
import { FirstArgument } from '../../../common/util/types'
|
import { FirstArgument } from '../../../common/util/types'
|
||||||
import { DPM, FreeResponse, FullContract } from '../../../common/contract'
|
import { DPM, FreeResponse, FullContract } from '../../../common/contract'
|
||||||
|
import { contractTextDetails } from '../../components/contract/contract-details'
|
||||||
|
|
||||||
export const getStaticProps = fromPropz(getStaticPropz)
|
export const getStaticProps = fromPropz(getStaticPropz)
|
||||||
export async function getStaticPropz(props: {
|
export async function getStaticPropz(props: {
|
||||||
|
|
|
@ -8,10 +8,8 @@ import { Spacer } from '../components/layout/spacer'
|
||||||
import { useUser } from '../hooks/use-user'
|
import { useUser } from '../hooks/use-user'
|
||||||
import { Contract, contractPath } from '../lib/firebase/contracts'
|
import { Contract, contractPath } from '../lib/firebase/contracts'
|
||||||
import { createContract } from '../lib/firebase/api-call'
|
import { createContract } from '../lib/firebase/api-call'
|
||||||
import { BuyAmountInput } from '../components/amount-input'
|
|
||||||
import { FIXED_ANTE, MINIMUM_ANTE } from '../../common/antes'
|
import { FIXED_ANTE, MINIMUM_ANTE } from '../../common/antes'
|
||||||
import { InfoTooltip } from '../components/info-tooltip'
|
import { InfoTooltip } from '../components/info-tooltip'
|
||||||
import { CREATOR_FEE } from '../../common/fees'
|
|
||||||
import { Page } from '../components/page'
|
import { Page } from '../components/page'
|
||||||
import { Title } from '../components/title'
|
import { Title } from '../components/title'
|
||||||
import { ProbabilitySelector } from '../components/probability-selector'
|
import { ProbabilitySelector } from '../components/probability-selector'
|
||||||
|
@ -39,6 +37,7 @@ export default function Create() {
|
||||||
<Textarea
|
<Textarea
|
||||||
placeholder="e.g. Will the Democrats win the 2024 US presidential election?"
|
placeholder="e.g. Will the Democrats win the 2024 US presidential election?"
|
||||||
className="input input-bordered resize-none"
|
className="input input-bordered resize-none"
|
||||||
|
autoFocus
|
||||||
value={question}
|
value={question}
|
||||||
onChange={(e) => setQuestion(e.target.value || '')}
|
onChange={(e) => setQuestion(e.target.value || '')}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -9,9 +9,9 @@ import { DOMAIN } from '../../../../common/envs/constants'
|
||||||
import { AnswersGraph } from '../../../components/answers/answers-graph'
|
import { AnswersGraph } from '../../../components/answers/answers-graph'
|
||||||
import {
|
import {
|
||||||
BinaryResolutionOrChance,
|
BinaryResolutionOrChance,
|
||||||
ContractDetails,
|
|
||||||
FreeResponseResolutionOrChance,
|
FreeResponseResolutionOrChance,
|
||||||
} from '../../../components/contract/contract-card'
|
} from '../../../components/contract/contract-card'
|
||||||
|
import { ContractDetails } from '../../../components/contract/contract-details'
|
||||||
import { ContractProbGraph } from '../../../components/contract/contract-prob-graph'
|
import { ContractProbGraph } from '../../../components/contract/contract-prob-graph'
|
||||||
import { Col } from '../../../components/layout/col'
|
import { Col } from '../../../components/layout/col'
|
||||||
import { Row } from '../../../components/layout/row'
|
import { Row } from '../../../components/layout/row'
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
const defaultTheme = require('tailwindcss/defaultTheme')
|
const defaultTheme = require('tailwindcss/defaultTheme')
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
mode: 'jit',
|
content: [
|
||||||
purge: ['./pages/**/*.{js,ts,jsx,tsx}', './components/**/*.{js,ts,jsx,tsx}'],
|
'./pages/**/*.{js,ts,jsx,tsx}',
|
||||||
darkMode: false, // or 'media' or 'class'
|
'./components/**/*.{js,ts,jsx,tsx}',
|
||||||
|
],
|
||||||
theme: {
|
theme: {
|
||||||
fontFamily: Object.assign(
|
fontFamily: Object.assign(
|
||||||
{ ...defaultTheme.fontFamily },
|
{ ...defaultTheme.fontFamily },
|
||||||
|
@ -18,9 +19,6 @@ module.exports = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
variants: {
|
|
||||||
extend: {},
|
|
||||||
},
|
|
||||||
plugins: [
|
plugins: [
|
||||||
require('@tailwindcss/forms'),
|
require('@tailwindcss/forms'),
|
||||||
require('@tailwindcss/typography'),
|
require('@tailwindcss/typography'),
|
||||||
|
|
|
@ -16,5 +16,5 @@
|
||||||
"incremental": true
|
"incremental": true
|
||||||
},
|
},
|
||||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "../common/**/*.ts"],
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "../common/**/*.ts"],
|
||||||
"exclude": ["node_modules"]
|
"exclude": ["node_modules", ".next"]
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user