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
|
||||
|
||||
.idea/
|
||||
.vercel
|
||||
node_modules
|
||||
|
|
|
@ -18,7 +18,14 @@ import {
|
|||
getDpmProbabilityAfterSale,
|
||||
} from './calculate-dpm'
|
||||
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>) {
|
||||
return contract.mechanism === 'cpmm-1'
|
||||
|
@ -170,3 +177,15 @@ export function getContractBetNullMetrics() {
|
|||
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
|
||||
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;
|
||||
}
|
||||
|
||||
match /private-users/{userId}/latency/{loadTimeId} {
|
||||
allow create: if userId == request.auth.uid;
|
||||
}
|
||||
|
||||
match /contracts/{contractId} {
|
||||
allow read;
|
||||
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
|
||||
1. `$ yarn global add firebase-tools` to install the Firebase CLI globally
|
||||
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
|
||||
5. `$ firebase functions:config:get > .runtimeconfig.json` to cache secrets for local dev (TODO: maybe not for Manifold)
|
||||
|
||||
## Developing locally
|
||||
|
||||
1. `$ firebase login` if you aren't logged into Firebase via commandline yet.
|
||||
2. `$ yarn dev` to spin up the emulators
|
||||
0. `$ yarn dev` to spin up the emulators
|
||||
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 =(
|
||||
3. Connect to emulators by enabling `functions.useEmulator('localhost', 5001)`
|
||||
1. Connect to emulators by enabling `functions.useEmulator('localhost', 5001)`
|
||||
|
||||
## Debugging
|
||||
|
||||
|
|
|
@ -1,15 +1,8 @@
|
|||
import * as admin from 'firebase-admin'
|
||||
import * as _ from 'lodash'
|
||||
|
||||
// Generate your own private key, and set the path below:
|
||||
// https://console.firebase.google.com/u/0/project/mantic-markets/settings/serviceaccounts/adminsdk
|
||||
|
||||
// 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 { initAdmin } from './script-init'
|
||||
initAdmin()
|
||||
|
||||
import { getUserByUsername } from '../utils'
|
||||
import { changeUser } from '../change-user-info'
|
||||
|
|
|
@ -2,7 +2,7 @@ import * as admin from 'firebase-admin'
|
|||
import * as _ from 'lodash'
|
||||
|
||||
import { initAdmin } from './script-init'
|
||||
initAdmin('stephen')
|
||||
initAdmin()
|
||||
|
||||
import { Bet } from '../../../common/bet'
|
||||
import { getDpmProbability } from '../../../common/calculate-dpm'
|
||||
|
|
|
@ -2,7 +2,7 @@ import * as admin from 'firebase-admin'
|
|||
import * as _ from 'lodash'
|
||||
|
||||
import { initAdmin } from './script-init'
|
||||
initAdmin('stephen')
|
||||
initAdmin()
|
||||
|
||||
import { PrivateUser, STARTING_BALANCE, User } from '../../../common/user'
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ import * as _ from 'lodash'
|
|||
import * as fs from 'fs'
|
||||
|
||||
import { initAdmin } from './script-init'
|
||||
initAdmin('james')
|
||||
initAdmin()
|
||||
|
||||
import { Bet } from '../../../common/bet'
|
||||
import { Contract } from '../../../common/contract'
|
||||
|
|
|
@ -2,7 +2,7 @@ import * as admin from 'firebase-admin'
|
|||
import * as _ from 'lodash'
|
||||
|
||||
import { initAdmin } from './script-init'
|
||||
initAdmin('james')
|
||||
initAdmin()
|
||||
|
||||
import { getValues } from '../utils'
|
||||
import { Fold } from '../../../common/fold'
|
||||
|
|
|
@ -2,7 +2,7 @@ import * as admin from 'firebase-admin'
|
|||
import * as _ from 'lodash'
|
||||
|
||||
import { initAdmin } from './script-init'
|
||||
initAdmin('james')
|
||||
initAdmin()
|
||||
|
||||
import { Contract } from '../../../common/contract'
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ import * as admin from 'firebase-admin'
|
|||
import * as _ from 'lodash'
|
||||
|
||||
import { initAdmin } from './script-init'
|
||||
initAdmin('james')
|
||||
initAdmin()
|
||||
|
||||
import { Bet } from '../../../common/bet'
|
||||
import { Contract } from '../../../common/contract'
|
||||
|
|
|
@ -2,7 +2,7 @@ import * as admin from 'firebase-admin'
|
|||
import * as _ from 'lodash'
|
||||
|
||||
import { initAdmin } from './script-init'
|
||||
initAdmin('stephenDev')
|
||||
initAdmin()
|
||||
|
||||
import {
|
||||
Binary,
|
||||
|
|
|
@ -2,7 +2,7 @@ import * as admin from 'firebase-admin'
|
|||
import * as _ from 'lodash'
|
||||
|
||||
import { initAdmin } from './script-init'
|
||||
initAdmin('stephenDev')
|
||||
initAdmin()
|
||||
|
||||
import { Binary, Contract, DPM, FullContract } from '../../../common/contract'
|
||||
import { Bet } from '../../../common/bet'
|
||||
|
|
|
@ -2,7 +2,7 @@ import * as admin from 'firebase-admin'
|
|||
import * as _ from 'lodash'
|
||||
|
||||
import { initAdmin } from './script-init'
|
||||
initAdmin('james')
|
||||
initAdmin()
|
||||
|
||||
import { Bet } from '../../../common/bet'
|
||||
import { Contract } from '../../../common/contract'
|
||||
|
|
|
@ -2,7 +2,7 @@ import * as admin from 'firebase-admin'
|
|||
import * as _ from 'lodash'
|
||||
|
||||
import { initAdmin } from './script-init'
|
||||
initAdmin('james')
|
||||
initAdmin()
|
||||
|
||||
import { Bet } from '../../../common/bet'
|
||||
import { Contract } from '../../../common/contract'
|
||||
|
|
|
@ -2,7 +2,7 @@ import * as admin from 'firebase-admin'
|
|||
import * as _ from 'lodash'
|
||||
|
||||
import { initAdmin } from './script-init'
|
||||
initAdmin('james')
|
||||
initAdmin()
|
||||
|
||||
import { Bet } from '../../../common/bet'
|
||||
import { Contract } from '../../../common/contract'
|
||||
|
|
|
@ -2,7 +2,7 @@ import * as admin from 'firebase-admin'
|
|||
import * as _ from 'lodash'
|
||||
|
||||
import { initAdmin } from './script-init'
|
||||
initAdmin('stephenDev')
|
||||
initAdmin()
|
||||
|
||||
import { Contract } from '../../../common/contract'
|
||||
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'
|
||||
|
||||
// Generate your own private key, and set the path below:
|
||||
// Prod:
|
||||
// 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
|
||||
// First, generate a private key from the Google service account management page:
|
||||
// Prod: 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
|
||||
// Then set GOOGLE_APPLICATION_CREDENTIALS_PROD or GOOGLE_APPLICATION_CREDENTIALS_DEV to the path of the key.
|
||||
|
||||
const pathsToPrivateKey = {
|
||||
james:
|
||||
'/Users/jahooma/mantic-markets-firebase-adminsdk-1ep46-820891bb87.json',
|
||||
jamesDev:
|
||||
'/Users/jahooma/dev-mantic-markets-firebase-adminsdk-sir5m-f38cdbee37.json',
|
||||
stephen:
|
||||
'../../../../../../Downloads/mantic-markets-firebase-adminsdk-1ep46-351a65eca3.json',
|
||||
stephenDev:
|
||||
'../../../../../../Downloads/dev-mantic-markets-firebase-adminsdk-sir5m-b2d27f8970.json',
|
||||
// Then, to run a script, make sure you are pointing at the Firebase you intend to:
|
||||
// $ firebase use dev (or prod)
|
||||
//
|
||||
// Followed by, if you have https://github.com/TypeStrong/ts-node installed (recommended):
|
||||
// $ ts-node my-script.ts
|
||||
//
|
||||
// Or compile it and run the compiled version:
|
||||
// $ yarn build && ../../lib/functions/scripts/src/my-script.js
|
||||
|
||||
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 serviceAccount = require(pathsToPrivateKey[who])
|
||||
const getFirebaseActiveProject = (cwd: string) => {
|
||||
// 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({
|
||||
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 { initAdmin } from './script-init'
|
||||
initAdmin('jamesDev')
|
||||
initAdmin()
|
||||
|
||||
import { Contract } from '../../../common/contract'
|
||||
import { parseTags } from '../../../common/util/parse'
|
||||
|
|
|
@ -64,6 +64,7 @@ export function AnswerBetPanel(props: {
|
|||
if (result?.status === 'success') {
|
||||
setIsSubmitting(false)
|
||||
setBetAmount(undefined)
|
||||
props.closePanel()
|
||||
} else {
|
||||
setError(result?.error || 'Error placing bet')
|
||||
setIsSubmitting(false)
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import clsx from 'clsx'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import _ from 'lodash'
|
||||
|
||||
import { useUser } from '../hooks/use-user'
|
||||
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 { BuyAmountInput, SellAmountInput } from './amount-input'
|
||||
import { InfoTooltip } from './info-tooltip'
|
||||
import { BinaryOutcomeLabel, OutcomeLabel } from './outcome-label'
|
||||
import { BinaryOutcomeLabel } from './outcome-label'
|
||||
import {
|
||||
calculatePayoutAfterCorrectBet,
|
||||
calculateShares,
|
||||
|
@ -32,61 +31,30 @@ import {
|
|||
calculateCpmmSale,
|
||||
getCpmmProbability,
|
||||
} from '../../common/calculate-cpmm'
|
||||
import { Modal } from './layout/modal'
|
||||
import { SellRow } from './sell-row'
|
||||
import { useSaveShares } from './use-save-shares'
|
||||
|
||||
export function BetPanel(props: {
|
||||
contract: FullContract<DPM | CPMM, Binary>
|
||||
className?: string
|
||||
}) {
|
||||
const { contract, className } = props
|
||||
const { mechanism } = contract
|
||||
|
||||
const user = useUser()
|
||||
const userBets = useUserContractBets(user?.id, contract.id)
|
||||
|
||||
const [showSellModal, setShowSellModal] = useState(false)
|
||||
|
||||
const { yesShares, noShares } = useSaveShares(contract, userBets)
|
||||
|
||||
const shares = yesShares || noShares
|
||||
const sharesOutcome = yesShares ? 'YES' : noShares ? 'NO' : undefined
|
||||
const { yesFloorShares, noFloorShares } = useSaveShares(contract, userBets)
|
||||
const sharesOutcome = yesFloorShares
|
||||
? 'YES'
|
||||
: noFloorShares
|
||||
? 'NO'
|
||||
: undefined
|
||||
|
||||
return (
|
||||
<Col className={className}>
|
||||
{sharesOutcome && user && mechanism === 'cpmm-1' && (
|
||||
<Col className="rounded-t-md bg-gray-100 px-6 py-6">
|
||||
<Row className="items-center justify-between gap-2">
|
||||
<div>
|
||||
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>
|
||||
)}
|
||||
|
||||
<SellRow
|
||||
contract={contract}
|
||||
user={user}
|
||||
className={'rounded-t-md bg-gray-100 px-6 py-6'}
|
||||
/>
|
||||
<Col
|
||||
className={clsx(
|
||||
'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 { yesShares, noShares } = useSaveShares(contract, userBets)
|
||||
const { yesFloorShares, noFloorShares, yesShares, noShares } = useSaveShares(
|
||||
contract,
|
||||
userBets
|
||||
)
|
||||
|
||||
const shares = yesShares || noShares
|
||||
const sharesOutcome = yesShares ? 'YES' : noShares ? 'NO' : undefined
|
||||
const floorShares = yesFloorShares || noFloorShares
|
||||
const sharesOutcome = yesFloorShares
|
||||
? 'YES'
|
||||
: noFloorShares
|
||||
? 'NO'
|
||||
: undefined
|
||||
|
||||
useEffect(() => {
|
||||
// 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">
|
||||
<Row className="items-center justify-between gap-2">
|
||||
<div>
|
||||
You have {formatWithCommas(Math.floor(shares))}{' '}
|
||||
You have {formatWithCommas(floorShares)}{' '}
|
||||
<BinaryOutcomeLabel outcome={sharesOutcome} /> shares
|
||||
</div>
|
||||
|
||||
|
@ -394,7 +369,7 @@ function BuyPanel(props: {
|
|||
)
|
||||
}
|
||||
|
||||
function SellPanel(props: {
|
||||
export function SellPanel(props: {
|
||||
contract: FullContract<CPMM, Binary>
|
||||
userBets: Bet[]
|
||||
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 { BetPanelSwitcher } from './bet-panel'
|
||||
|
@ -6,6 +5,10 @@ import { Row } from './layout/row'
|
|||
import { YesNoSelector } from './yes-no-selector'
|
||||
import { Binary, CPMM, DPM, FullContract } from '../../common/contract'
|
||||
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.
|
||||
export default function BetRow(props: {
|
||||
|
@ -13,16 +16,19 @@ export default function BetRow(props: {
|
|||
className?: string
|
||||
labelClassName?: string
|
||||
}) {
|
||||
const { className, labelClassName } = props
|
||||
const { className, labelClassName, contract } = props
|
||||
const [open, setOpen] = useState(false)
|
||||
const [betChoice, setBetChoice] = useState<'YES' | 'NO' | undefined>(
|
||||
undefined
|
||||
)
|
||||
const user = useUser()
|
||||
const userBets = useUserContractBets(user?.id, contract.id)
|
||||
const { yesFloorShares, noFloorShares } = useSaveShares(contract, userBets)
|
||||
|
||||
return (
|
||||
<>
|
||||
<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)}>
|
||||
Place a trade
|
||||
</div> */}
|
||||
|
@ -32,12 +38,22 @@ export default function BetRow(props: {
|
|||
setOpen(true)
|
||||
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>
|
||||
<Modal open={open} setOpen={setOpen}>
|
||||
<BetPanelSwitcher
|
||||
contract={props.contract}
|
||||
title={props.contract.question}
|
||||
contract={contract}
|
||||
title={contract.question}
|
||||
selected={betChoice}
|
||||
onBetSuccess={() => setOpen(false)}
|
||||
/>
|
||||
|
|
|
@ -37,6 +37,8 @@ import {
|
|||
resolvedPayout,
|
||||
getContractBetNullMetrics,
|
||||
} 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 BetFilter = 'open' | 'closed' | 'resolved' | 'all'
|
||||
|
@ -67,6 +69,14 @@ export function BetsList(props: { user: User }) {
|
|||
}
|
||||
}, [bets])
|
||||
|
||||
const getTime = useTimeSinceFirstRender()
|
||||
useEffect(() => {
|
||||
if (bets && contracts) {
|
||||
trackLatency('portfolio', getTime())
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [!!bets, !!contracts])
|
||||
|
||||
if (!bets || !contracts) {
|
||||
return <LoadingIndicator />
|
||||
}
|
||||
|
|
|
@ -1,27 +1,15 @@
|
|||
import clsx from 'clsx'
|
||||
import Link from 'next/link'
|
||||
import _ from 'lodash'
|
||||
import { ClockIcon, DatabaseIcon, PencilIcon } from '@heroicons/react/outline'
|
||||
import { TrendingUpIcon } from '@heroicons/react/solid'
|
||||
import { Row } from '../layout/row'
|
||||
import { formatMoney, formatPercent } from '../../../common/util/format'
|
||||
import { UserLink } from '../user-page'
|
||||
import { formatPercent } from '../../../common/util/format'
|
||||
import {
|
||||
Contract,
|
||||
contractMetrics,
|
||||
contractPath,
|
||||
getBinaryProbPercent,
|
||||
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 { Spacer } from '../layout/spacer'
|
||||
import { useState } from 'react'
|
||||
import { ContractInfoDialog } from './contract-info-dialog'
|
||||
import { Bet } from '../../../common/bet'
|
||||
import {
|
||||
Binary,
|
||||
CPMM,
|
||||
|
@ -35,7 +23,8 @@ import {
|
|||
BinaryContractOutcomeLabel,
|
||||
FreeResponseOutcomeLabel,
|
||||
} from '../outcome-label'
|
||||
import { getOutcomeProbability } from '../../../common/calculate'
|
||||
import { getOutcomeProbability, getTopAnswer } from '../../../common/calculate'
|
||||
import { AbbrContractDetails } from './contract-details'
|
||||
|
||||
export function ContractCard(props: {
|
||||
contract: Contract
|
||||
|
@ -68,7 +57,7 @@ export function ContractCard(props: {
|
|||
<Row
|
||||
className={clsx(
|
||||
'justify-between gap-4',
|
||||
outcomeType === 'FREE_RESPONSE' && 'flex-col items-start'
|
||||
outcomeType === 'FREE_RESPONSE' && 'flex-col items-start !gap-2'
|
||||
)}
|
||||
>
|
||||
<p
|
||||
|
@ -85,6 +74,7 @@ export function ContractCard(props: {
|
|||
)}
|
||||
{outcomeType === 'FREE_RESPONSE' && (
|
||||
<FreeResponseResolutionOrChance
|
||||
className="self-end text-gray-600"
|
||||
contract={contract as FullContract<DPM, FreeResponse>}
|
||||
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: {
|
||||
contract: FreeResponseContract
|
||||
truncate: 'short' | 'long' | 'none'
|
||||
className?: string
|
||||
}) {
|
||||
const { contract, truncate } = props
|
||||
const { contract, truncate, className } = props
|
||||
const { resolution } = contract
|
||||
|
||||
const topAnswer = getTopAnswer(contract)
|
||||
|
||||
return (
|
||||
<Col className="text-xl">
|
||||
<Col className={clsx(resolution ? 'text-3xl' : 'text-xl', className)}>
|
||||
{resolution ? (
|
||||
<>
|
||||
<div className={clsx('text-base text-gray-500')}>Resolved</div>
|
||||
|
@ -162,13 +141,18 @@ export function FreeResponseResolutionOrChance(props: {
|
|||
contract={contract}
|
||||
resolution={resolution}
|
||||
truncate={truncate}
|
||||
answerClassName="text-xl"
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
topAnswer && (
|
||||
<Row className="flex-1 items-center justify-between gap-6">
|
||||
<AnswerLabel answer={topAnswer} truncate={truncate} />
|
||||
<Col className="text-primary">
|
||||
<Row className="items-center gap-6">
|
||||
<AnswerLabel
|
||||
className="!text-gray-600"
|
||||
answer={topAnswer}
|
||||
truncate={truncate}
|
||||
/>
|
||||
<Col className="text-primary text-3xl">
|
||||
<div>
|
||||
{formatPercent(getOutcomeProbability(contract, topAnswer.id))}
|
||||
</div>
|
||||
|
@ -180,209 +164,3 @@ export function FreeResponseResolutionOrChance(props: {
|
|||
</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 {
|
||||
FreeResponseResolutionOrChance,
|
||||
ContractDetails,
|
||||
BinaryResolutionOrChance,
|
||||
} from './contract-card'
|
||||
import { Bet } from '../../../common/bet'
|
||||
|
@ -17,6 +16,7 @@ import BetRow from '../bet-row'
|
|||
import { AnswersGraph } from '../answers/answers-graph'
|
||||
import { DPM, FreeResponse, FullContract } from '../../../common/contract'
|
||||
import { ContractDescription } from './contract-description'
|
||||
import { ContractDetails } from './contract-details'
|
||||
|
||||
export const ContractOverview = (props: {
|
||||
contract: Contract
|
||||
|
|
|
@ -9,6 +9,7 @@ import {
|
|||
UserIcon,
|
||||
UsersIcon,
|
||||
XIcon,
|
||||
SparklesIcon,
|
||||
} from '@heroicons/react/solid'
|
||||
import clsx from 'clsx'
|
||||
import Textarea from 'react-expanding-textarea'
|
||||
|
@ -46,6 +47,8 @@ import { useSaveSeenContract } from '../../hooks/use-seen-contracts'
|
|||
import { User } from '../../../common/user'
|
||||
import { Modal } from '../layout/modal'
|
||||
import { trackClick } from '../../lib/firebase/tracking'
|
||||
import { DAY_MS } from '../../../common/util/time'
|
||||
import NewContractBadge from '../new-contract-badge'
|
||||
|
||||
export function FeedItems(props: {
|
||||
contract: Contract
|
||||
|
@ -307,19 +310,17 @@ export function FeedQuestion(props: {
|
|||
contractPath?: string
|
||||
}) {
|
||||
const { contract, showDescription } = props
|
||||
const { creatorName, creatorUsername, question, resolution, outcomeType } =
|
||||
contract
|
||||
const {
|
||||
creatorName,
|
||||
creatorUsername,
|
||||
question,
|
||||
outcomeType,
|
||||
volume,
|
||||
createdTime,
|
||||
} = contract
|
||||
const { volumeLabel } = contractMetrics(contract)
|
||||
const isBinary = outcomeType === 'BINARY'
|
||||
|
||||
// const closeMessage =
|
||||
// contract.isResolved || !contract.closeTime ? null : (
|
||||
// <>
|
||||
// <span className="mx-2">•</span>
|
||||
// {contract.closeTime > Date.now() ? 'Closes' : 'Closed'}
|
||||
// <RelativeTimestamp time={contract.closeTime || 0} />
|
||||
// </>
|
||||
// )
|
||||
const isNew = createdTime > Date.now() - DAY_MS
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -336,10 +337,15 @@ export function FeedQuestion(props: {
|
|||
/>{' '}
|
||||
asked
|
||||
{/* Currently hidden on mobile; ideally we'd fit this in somewhere. */}
|
||||
<span className="float-right hidden text-gray-400 sm:inline">
|
||||
{volumeLabel}
|
||||
{/* {closeMessage} */}
|
||||
</span>
|
||||
<div className="relative -top-2 float-right ">
|
||||
{isNew || volume === 0 ? (
|
||||
<NewContractBadge />
|
||||
) : (
|
||||
<span className="hidden text-gray-400 sm:inline">
|
||||
{volumeLabel}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<Col className="items-start justify-between gap-2 sm:flex-row sm:gap-4">
|
||||
<Col>
|
||||
|
|
|
@ -158,6 +158,12 @@ export default function Sidebar() {
|
|||
buttonContent={<MoreButton />}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{user && (
|
||||
<Link href={'/create'}>
|
||||
<button className="btn btn-primary btn-md mt-4">Create Market</button>
|
||||
</Link>
|
||||
)}
|
||||
</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 { getProbability } from '../../common/calculate'
|
||||
import {
|
||||
|
@ -59,8 +60,9 @@ export function FreeResponseOutcomeLabel(props: {
|
|||
contract: FreeResponseContract
|
||||
resolution: string | 'CANCEL' | 'MKT'
|
||||
truncate: 'short' | 'long' | 'none'
|
||||
answerClassName?: string
|
||||
}) {
|
||||
const { contract, resolution, truncate } = props
|
||||
const { contract, resolution, truncate, answerClassName } = props
|
||||
|
||||
if (resolution === 'CANCEL') return <CancelLabel />
|
||||
if (resolution === 'MKT') return <MultiLabel />
|
||||
|
@ -68,7 +70,13 @@ export function FreeResponseOutcomeLabel(props: {
|
|||
const { answers } = contract
|
||||
const chosen = answers?.find((answer) => answer.id === resolution)
|
||||
if (!chosen) return <AnswerNumberLabel number={resolution} />
|
||||
return <AnswerLabel answer={chosen} truncate={truncate} />
|
||||
return (
|
||||
<AnswerLabel
|
||||
answer={chosen}
|
||||
truncate={truncate}
|
||||
className={answerClassName}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export function YesLabel() {
|
||||
|
@ -103,8 +111,9 @@ export function AnswerNumberLabel(props: { number: string }) {
|
|||
export function AnswerLabel(props: {
|
||||
answer: Answer
|
||||
truncate: 'short' | 'long' | 'none'
|
||||
className?: string
|
||||
}) {
|
||||
const { answer, truncate } = props
|
||||
const { answer, truncate, className } = props
|
||||
const { text } = answer
|
||||
|
||||
let truncated = text
|
||||
|
@ -114,5 +123,5 @@ export function AnswerLabel(props: {
|
|||
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">
|
||||
{user.website && (
|
||||
<SiteLink href={user.website}>
|
||||
<SiteLink
|
||||
href={
|
||||
'https://' +
|
||||
user.website.replace('http://', '').replace('https://', '')
|
||||
}
|
||||
>
|
||||
<Row className="items-center gap-1">
|
||||
<LinkIcon className="h-4 w-4" />
|
||||
<span className="text-sm text-gray-500">{user.website}</span>
|
||||
|
@ -96,7 +101,13 @@ export function UserPage(props: { user: User; currentUser?: User }) {
|
|||
)}
|
||||
|
||||
{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">
|
||||
<img
|
||||
src="/twitter-logo.svg"
|
||||
|
|
|
@ -9,40 +9,57 @@ export function YesNoSelector(props: {
|
|||
onSelect: (selected: 'YES' | 'NO') => void
|
||||
className?: 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 =
|
||||
'inline-flex flex-1 items-center justify-center rounded-3xl border-2 p-2'
|
||||
|
||||
return (
|
||||
<Row className={clsx('space-x-3', className)}>
|
||||
<button
|
||||
className={clsx(
|
||||
commonClassNames,
|
||||
'hover:bg-primary-focus border-primary hover:border-primary-focus hover:text-white',
|
||||
selected == 'YES'
|
||||
? 'bg-primary text-white'
|
||||
: 'text-primary bg-transparent',
|
||||
btnClassName
|
||||
)}
|
||||
onClick={() => onSelect('YES')}
|
||||
>
|
||||
Bet YES
|
||||
</button>
|
||||
<button
|
||||
className={clsx(
|
||||
commonClassNames,
|
||||
'border-red-400 hover:border-red-500 hover:bg-red-500 hover:text-white',
|
||||
selected == 'NO'
|
||||
? 'bg-red-400 text-white'
|
||||
: 'bg-transparent text-red-400',
|
||||
btnClassName
|
||||
)}
|
||||
onClick={() => onSelect('NO')}
|
||||
>
|
||||
Bet NO
|
||||
</button>
|
||||
{replaceYesButton ? (
|
||||
replaceYesButton
|
||||
) : (
|
||||
<button
|
||||
className={clsx(
|
||||
commonClassNames,
|
||||
'hover:bg-primary-focus border-primary hover:border-primary-focus hover:text-white',
|
||||
selected == 'YES'
|
||||
? 'bg-primary text-white'
|
||||
: 'text-primary bg-transparent',
|
||||
btnClassName
|
||||
)}
|
||||
onClick={() => onSelect('YES')}
|
||||
>
|
||||
Bet YES
|
||||
</button>
|
||||
)}
|
||||
{replaceNoButton ? (
|
||||
replaceNoButton
|
||||
) : (
|
||||
<button
|
||||
className={clsx(
|
||||
commonClassNames,
|
||||
'border-red-400 hover:border-red-500 hover:bg-red-500 hover:text-white',
|
||||
selected == 'NO'
|
||||
? 'bg-red-400 text-white'
|
||||
: 'bg-transparent text-red-400',
|
||||
btnClassName
|
||||
)}
|
||||
onClick={() => onSelect('NO')}
|
||||
>
|
||||
Bet NO
|
||||
</button>
|
||||
)}
|
||||
</Row>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -8,6 +8,14 @@ import { logInterpolation } from '../../common/util/math'
|
|||
import { getRecommendedContracts } from '../../common/recommended-contracts'
|
||||
import { useSeenContracts } from './use-seen-contracts'
|
||||
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
|
||||
|
||||
|
@ -29,8 +37,15 @@ export const useAlgoFeed = (
|
|||
|
||||
const [algoFeed, setAlgoFeed] = useState<Contract[]>([])
|
||||
|
||||
const getTime = useTimeSinceFirstRender()
|
||||
|
||||
useEffect(() => {
|
||||
if (initialContracts && initialBets && initialComments) {
|
||||
if (
|
||||
initialContracts &&
|
||||
initialBets &&
|
||||
initialComments &&
|
||||
yourBetContractIds
|
||||
) {
|
||||
const eligibleContracts = initialContracts.filter(
|
||||
(c) => !c.isResolved && (c.closeTime ?? Infinity) > Date.now()
|
||||
)
|
||||
|
@ -42,6 +57,7 @@ export const useAlgoFeed = (
|
|||
seenContracts
|
||||
)
|
||||
setAlgoFeed(contracts)
|
||||
trackLatency('feed', getTime())
|
||||
}
|
||||
}, [
|
||||
initialBets,
|
||||
|
@ -49,6 +65,7 @@ export const useAlgoFeed = (
|
|||
initialContracts,
|
||||
seenContracts,
|
||||
yourBetContractIds,
|
||||
getTime,
|
||||
])
|
||||
|
||||
return algoFeed
|
||||
|
@ -120,11 +137,13 @@ function getContractsActivityScores(
|
|||
)
|
||||
|
||||
const scoredContracts = contracts.map((contract) => {
|
||||
const { outcomeType } = contract
|
||||
|
||||
const seenTime = seenContracts[contract.id]
|
||||
const lastCommentTime = contractMostRecentComment[contract.id]?.createdTime
|
||||
const hasNewComments =
|
||||
!seenTime || (lastCommentTime && lastCommentTime > seenTime)
|
||||
const newCommentScore = hasNewComments ? 1 : 0.75
|
||||
const newCommentScore = hasNewComments ? 1 : 0.5
|
||||
|
||||
const commentCount = contractComments[contract.id]?.length ?? 0
|
||||
const betCount = contractBets[contract.id]?.length ?? 0
|
||||
|
@ -132,25 +151,39 @@ function getContractsActivityScores(
|
|||
const activityCountScore =
|
||||
0.5 + 0.5 * logInterpolation(0, 200, activtyCount)
|
||||
|
||||
const lastBetTime = contractMostRecentBet[contract.id]?.createdTime
|
||||
const timeSinceLastBet = !lastBetTime
|
||||
? contract.createdTime
|
||||
: Date.now() - lastBetTime
|
||||
const daysAgo = timeSinceLastBet / oneDayMs
|
||||
const lastBetTime =
|
||||
contractMostRecentBet[contract.id]?.createdTime ?? contract.createdTime
|
||||
const timeSinceLastBet = Date.now() - lastBetTime
|
||||
const daysAgo = timeSinceLastBet / DAY_MS
|
||||
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.
|
||||
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)
|
||||
}
|
||||
|
||||
const oneDayMs = 24 * 60 * 60 * 1000
|
||||
|
||||
function getSeenContractsScore(
|
||||
contract: Contract,
|
||||
seenContracts: { [contractId: string]: number }
|
||||
|
@ -160,7 +193,7 @@ function getSeenContractsScore(
|
|||
return 1
|
||||
}
|
||||
|
||||
const daysAgo = (Date.now() - lastSeen) / oneDayMs
|
||||
const daysAgo = (Date.now() - lastSeen) / DAY_MS
|
||||
|
||||
if (daysAgo < 0.5) {
|
||||
const frac = logInterpolation(0, 0.5, daysAgo)
|
||||
|
|
|
@ -6,6 +6,7 @@ import {
|
|||
listenForContracts,
|
||||
listenForHotContracts,
|
||||
listenForInactiveContracts,
|
||||
listenForNewContracts,
|
||||
} from '../lib/firebase/contracts'
|
||||
import { listenForTaggedContracts } from '../lib/firebase/folds'
|
||||
|
||||
|
@ -20,13 +21,22 @@ export const useContracts = () => {
|
|||
}
|
||||
|
||||
export const useActiveContracts = () => {
|
||||
const [contracts, setContracts] = useState<Contract[] | undefined>()
|
||||
const [activeContracts, setActiveContracts] = useState<
|
||||
Contract[] | undefined
|
||||
>()
|
||||
const [newContracts, setNewContracts] = useState<Contract[] | undefined>()
|
||||
|
||||
useEffect(() => {
|
||||
return listenForActiveContracts(setContracts)
|
||||
return listenForActiveContracts(setActiveContracts)
|
||||
}, [])
|
||||
|
||||
return contracts
|
||||
useEffect(() => {
|
||||
return listenForNewContracts(setNewContracts)
|
||||
}, [])
|
||||
|
||||
if (!activeContracts || !newContracts) return undefined
|
||||
|
||||
return [...activeContracts, ...newContracts]
|
||||
}
|
||||
|
||||
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) => {
|
||||
const [contractIds, setContractIds] = useState<string[]>([])
|
||||
const [contractIds, setContractIds] = useState<string[] | undefined>()
|
||||
|
||||
useEffect(() => {
|
||||
const key = `user-bet-contractIds-${userId}`
|
||||
const userBetContractJson = localStorage.getItem(key)
|
||||
if (userBetContractJson) {
|
||||
setContractIds(JSON.parse(userBetContractJson))
|
||||
if (userId) {
|
||||
const key = `user-bet-contractIds-${userId}`
|
||||
const userBetContractJson = localStorage.getItem(key)
|
||||
if (userBetContractJson) {
|
||||
setContractIds(JSON.parse(userBetContractJson))
|
||||
}
|
||||
}
|
||||
}, [userId])
|
||||
|
||||
|
|
|
@ -22,6 +22,7 @@ import { getDpmProbability } from '../../../common/calculate-dpm'
|
|||
import { createRNG, shuffle } from '../../../common/util/random'
|
||||
import { getCpmmProbability } from '../../../common/calculate-cpmm'
|
||||
import { formatMoney, formatPercent } from '../../../common/util/format'
|
||||
import { DAY_MS } from '../../../common/util/time'
|
||||
export type { Contract }
|
||||
|
||||
export function contractPath(contract: Contract) {
|
||||
|
@ -162,6 +163,19 @@ export function listenForInactiveContracts(
|
|||
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(
|
||||
contractId: string,
|
||||
setContract: (contract: Contract | null) => void
|
||||
|
|
|
@ -2,7 +2,7 @@ import { doc, collection, setDoc } from 'firebase/firestore'
|
|||
import _ from 'lodash'
|
||||
|
||||
import { db } from './init'
|
||||
import { ClickEvent, View } from '../../../common/tracking'
|
||||
import { ClickEvent, LatencyEvent, View } from '../../../common/tracking'
|
||||
import { listenForLogin, User } from './users'
|
||||
|
||||
let user: User | null = null
|
||||
|
@ -34,3 +34,19 @@ export async function trackClick(contractId: string) {
|
|||
|
||||
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,
|
||||
"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\"",
|
||||
"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: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",
|
||||
|
|
|
@ -19,7 +19,6 @@ export default function Custom404() {
|
|||
src="https://discord.com/widget?id=915138780216823849&theme=dark"
|
||||
width="350"
|
||||
height="500"
|
||||
allowTransparency={true}
|
||||
frameBorder="0"
|
||||
sandbox="allow-popups allow-popups-to-escape-sandbox allow-same-origin allow-scripts"
|
||||
></iframe>
|
||||
|
|
|
@ -18,7 +18,6 @@ import {
|
|||
} from '../../lib/firebase/contracts'
|
||||
import { SEO } from '../../components/SEO'
|
||||
import { Page } from '../../components/page'
|
||||
import { contractTextDetails } from '../../components/contract/contract-card'
|
||||
import { Bet, listAllBets } from '../../lib/firebase/bets'
|
||||
import { Comment, listAllComments } from '../../lib/firebase/comments'
|
||||
import Custom404 from '../404'
|
||||
|
@ -33,6 +32,7 @@ import { useUserById } from '../../hooks/use-users'
|
|||
import { ContractTabs } from '../../components/contract/contract-tabs'
|
||||
import { FirstArgument } from '../../../common/util/types'
|
||||
import { DPM, FreeResponse, FullContract } from '../../../common/contract'
|
||||
import { contractTextDetails } from '../../components/contract/contract-details'
|
||||
|
||||
export const getStaticProps = fromPropz(getStaticPropz)
|
||||
export async function getStaticPropz(props: {
|
||||
|
|
|
@ -8,10 +8,8 @@ import { Spacer } from '../components/layout/spacer'
|
|||
import { useUser } from '../hooks/use-user'
|
||||
import { Contract, contractPath } from '../lib/firebase/contracts'
|
||||
import { createContract } from '../lib/firebase/api-call'
|
||||
import { BuyAmountInput } from '../components/amount-input'
|
||||
import { FIXED_ANTE, MINIMUM_ANTE } from '../../common/antes'
|
||||
import { InfoTooltip } from '../components/info-tooltip'
|
||||
import { CREATOR_FEE } from '../../common/fees'
|
||||
import { Page } from '../components/page'
|
||||
import { Title } from '../components/title'
|
||||
import { ProbabilitySelector } from '../components/probability-selector'
|
||||
|
@ -39,6 +37,7 @@ export default function Create() {
|
|||
<Textarea
|
||||
placeholder="e.g. Will the Democrats win the 2024 US presidential election?"
|
||||
className="input input-bordered resize-none"
|
||||
autoFocus
|
||||
value={question}
|
||||
onChange={(e) => setQuestion(e.target.value || '')}
|
||||
/>
|
||||
|
|
|
@ -9,9 +9,9 @@ import { DOMAIN } from '../../../../common/envs/constants'
|
|||
import { AnswersGraph } from '../../../components/answers/answers-graph'
|
||||
import {
|
||||
BinaryResolutionOrChance,
|
||||
ContractDetails,
|
||||
FreeResponseResolutionOrChance,
|
||||
} from '../../../components/contract/contract-card'
|
||||
import { ContractDetails } from '../../../components/contract/contract-details'
|
||||
import { ContractProbGraph } from '../../../components/contract/contract-prob-graph'
|
||||
import { Col } from '../../../components/layout/col'
|
||||
import { Row } from '../../../components/layout/row'
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
const defaultTheme = require('tailwindcss/defaultTheme')
|
||||
|
||||
module.exports = {
|
||||
mode: 'jit',
|
||||
purge: ['./pages/**/*.{js,ts,jsx,tsx}', './components/**/*.{js,ts,jsx,tsx}'],
|
||||
darkMode: false, // or 'media' or 'class'
|
||||
content: [
|
||||
'./pages/**/*.{js,ts,jsx,tsx}',
|
||||
'./components/**/*.{js,ts,jsx,tsx}',
|
||||
],
|
||||
theme: {
|
||||
fontFamily: Object.assign(
|
||||
{ ...defaultTheme.fontFamily },
|
||||
|
@ -18,9 +19,6 @@ module.exports = {
|
|||
},
|
||||
},
|
||||
},
|
||||
variants: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [
|
||||
require('@tailwindcss/forms'),
|
||||
require('@tailwindcss/typography'),
|
||||
|
|
|
@ -16,5 +16,5 @@
|
|||
"incremental": true
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "../common/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
"exclude": ["node_modules", ".next"]
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user