Merge branch 'main' into automated-market-resolution
# Conflicts: # web/components/choices-toggle-group.tsx # web/pages/create.tsx
This commit is contained in:
commit
f0dc00e6ad
|
@ -9,11 +9,18 @@ module.exports = {
|
||||||
{
|
{
|
||||||
files: ['**/*.ts'],
|
files: ['**/*.ts'],
|
||||||
plugins: ['@typescript-eslint'],
|
plugins: ['@typescript-eslint'],
|
||||||
|
extends: ['plugin:@typescript-eslint/recommended'],
|
||||||
parser: '@typescript-eslint/parser',
|
parser: '@typescript-eslint/parser',
|
||||||
|
parserOptions: {
|
||||||
|
tsconfigRootDir: __dirname,
|
||||||
|
project: ['./tsconfig.json'],
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
'@typescript-eslint/no-explicit-any': 'off',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
rules: {
|
rules: {
|
||||||
'no-unused-vars': 'off',
|
|
||||||
'no-constant-condition': ['error', { checkLoops: false }],
|
'no-constant-condition': ['error', { checkLoops: false }],
|
||||||
'lodash/import-scope': [2, 'member'],
|
'lodash/import-scope': [2, 'member'],
|
||||||
},
|
},
|
||||||
|
|
|
@ -170,7 +170,7 @@ export function calculateNumericDpmShares(
|
||||||
([amount]) => amount
|
([amount]) => amount
|
||||||
).map(([, i]) => i)
|
).map(([, i]) => i)
|
||||||
|
|
||||||
for (let i of order) {
|
for (const i of order) {
|
||||||
const [bucket, bet] = bets[i]
|
const [bucket, bet] = bets[i]
|
||||||
shares[i] = calculateDpmShares(totalShares, bet, bucket)
|
shares[i] = calculateDpmShares(totalShares, bet, bucket)
|
||||||
totalShares = addObjects(totalShares, { [bucket]: shares[i] })
|
totalShares = addObjects(totalShares, { [bucket]: shares[i] })
|
||||||
|
|
|
@ -5,13 +5,13 @@ import { THEOREMONE_CONFIG } from './theoremone'
|
||||||
|
|
||||||
export const ENV = process.env.NEXT_PUBLIC_FIREBASE_ENV ?? 'PROD'
|
export const ENV = process.env.NEXT_PUBLIC_FIREBASE_ENV ?? 'PROD'
|
||||||
|
|
||||||
const CONFIGS = {
|
const CONFIGS: { [env: string]: EnvConfig } = {
|
||||||
PROD: PROD_CONFIG,
|
PROD: PROD_CONFIG,
|
||||||
DEV: DEV_CONFIG,
|
DEV: DEV_CONFIG,
|
||||||
THEOREMONE: THEOREMONE_CONFIG,
|
THEOREMONE: THEOREMONE_CONFIG,
|
||||||
}
|
}
|
||||||
// @ts-ignore
|
|
||||||
export const ENV_CONFIG: EnvConfig = CONFIGS[ENV]
|
export const ENV_CONFIG = CONFIGS[ENV]
|
||||||
|
|
||||||
export function isWhitelisted(email?: string) {
|
export function isWhitelisted(email?: string) {
|
||||||
if (!ENV_CONFIG.whitelistEmail) {
|
if (!ENV_CONFIG.whitelistEmail) {
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import { range } from 'lodash'
|
import { range } from 'lodash'
|
||||||
import { PHANTOM_ANTE } from './antes'
|
|
||||||
import {
|
import {
|
||||||
Binary,
|
Binary,
|
||||||
Contract,
|
Contract,
|
||||||
|
@ -14,7 +13,6 @@ import {
|
||||||
import { User } from './user'
|
import { User } from './user'
|
||||||
import { parseTags } from './util/parse'
|
import { parseTags } from './util/parse'
|
||||||
import { removeUndefinedProps } from './util/object'
|
import { removeUndefinedProps } from './util/object'
|
||||||
import { calcDpmInitialPool } from './calculate-dpm'
|
|
||||||
|
|
||||||
export function getNewContract(
|
export function getNewContract(
|
||||||
id: string,
|
id: string,
|
||||||
|
@ -86,6 +84,9 @@ export function getNewContract(
|
||||||
return contract as Contract
|
return contract as Contract
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
import { PHANTOM_ANTE } from './antes'
|
||||||
|
import { calcDpmInitialPool } from './calculate-dpm'
|
||||||
const getBinaryDpmProps = (initialProb: number, ante: number) => {
|
const getBinaryDpmProps = (initialProb: number, ante: number) => {
|
||||||
const { sharesYes, sharesNo, poolYes, poolNo, phantomYes, phantomNo } =
|
const { sharesYes, sharesNo, poolYes, poolNo, phantomYes, phantomNo } =
|
||||||
calcDpmInitialPool(initialProb, ante, PHANTOM_ANTE)
|
calcDpmInitialPool(initialProb, ante, PHANTOM_ANTE)
|
||||||
|
@ -102,6 +103,7 @@ const getBinaryDpmProps = (initialProb: number, ante: number) => {
|
||||||
|
|
||||||
return system
|
return system
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
const getBinaryCpmmProps = (initialProb: number, ante: number) => {
|
const getBinaryCpmmProps = (initialProb: number, ante: number) => {
|
||||||
const pool = { YES: ante, NO: ante }
|
const pool = { YES: ante, NO: ante }
|
||||||
|
@ -162,11 +164,3 @@ const getNumericProps = (
|
||||||
|
|
||||||
return system
|
return system
|
||||||
}
|
}
|
||||||
|
|
||||||
const getMultiProps = (
|
|
||||||
outcomes: string[],
|
|
||||||
initialProbs: number[],
|
|
||||||
ante: number
|
|
||||||
) => {
|
|
||||||
// Not implemented.
|
|
||||||
}
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { Bet } from './bet'
|
||||||
import { Binary, Contract, FullContract } from './contract'
|
import { Binary, Contract, FullContract } from './contract'
|
||||||
import { getPayouts } from './payouts'
|
import { getPayouts } from './payouts'
|
||||||
|
|
||||||
export function scoreCreators(contracts: Contract[], bets: Bet[][]) {
|
export function scoreCreators(contracts: Contract[]) {
|
||||||
const creatorScore = mapValues(
|
const creatorScore = mapValues(
|
||||||
groupBy(contracts, ({ creatorId }) => creatorId),
|
groupBy(contracts, ({ creatorId }) => creatorId),
|
||||||
(contracts) => sumBy(contracts, ({ pool }) => pool.YES + pool.NO)
|
(contracts) => sumBy(contracts, ({ pool }) => pool.YES + pool.NO)
|
||||||
|
|
12
common/tsconfig.json
Normal file
12
common/tsconfig.json
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": "../",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"noImplicitReturns": true,
|
||||||
|
"outDir": "lib",
|
||||||
|
"sourceMap": true,
|
||||||
|
"strict": true,
|
||||||
|
"target": "es2017"
|
||||||
|
},
|
||||||
|
"include": ["**/*.ts"]
|
||||||
|
}
|
|
@ -1,9 +1,9 @@
|
||||||
import { union } from 'lodash'
|
import { union } from 'lodash'
|
||||||
|
|
||||||
export const removeUndefinedProps = <T>(obj: T): T => {
|
export const removeUndefinedProps = <T>(obj: T): T => {
|
||||||
let newObj: any = {}
|
const newObj: any = {}
|
||||||
|
|
||||||
for (let key of Object.keys(obj)) {
|
for (const key of Object.keys(obj)) {
|
||||||
if ((obj as any)[key] !== undefined) newObj[key] = (obj as any)[key]
|
if ((obj as any)[key] !== undefined) newObj[key] = (obj as any)[key]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,7 +17,7 @@ export const addObjects = <T extends { [key: string]: number }>(
|
||||||
const keys = union(Object.keys(obj1), Object.keys(obj2))
|
const keys = union(Object.keys(obj1), Object.keys(obj2))
|
||||||
const newObj = {} as any
|
const newObj = {} as any
|
||||||
|
|
||||||
for (let key of keys) {
|
for (const key of keys) {
|
||||||
newObj[key] = (obj1[key] ?? 0) + (obj2[key] ?? 0)
|
newObj[key] = (obj1[key] ?? 0) + (obj2[key] ?? 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,8 @@ export const randomString = (length = 12) =>
|
||||||
|
|
||||||
export function genHash(str: string) {
|
export function genHash(str: string) {
|
||||||
// xmur3
|
// xmur3
|
||||||
for (var i = 0, h = 1779033703 ^ str.length; i < str.length; i++) {
|
let h: number
|
||||||
|
for (let i = 0, h = 1779033703 ^ str.length; i < str.length; i++) {
|
||||||
h = Math.imul(h ^ str.charCodeAt(i), 3432918353)
|
h = Math.imul(h ^ str.charCodeAt(i), 3432918353)
|
||||||
h = (h << 13) | (h >>> 19)
|
h = (h << 13) | (h >>> 19)
|
||||||
}
|
}
|
||||||
|
@ -28,7 +29,7 @@ export function createRNG(seed: string) {
|
||||||
b >>>= 0
|
b >>>= 0
|
||||||
c >>>= 0
|
c >>>= 0
|
||||||
d >>>= 0
|
d >>>= 0
|
||||||
var t = (a + b) | 0
|
let t = (a + b) | 0
|
||||||
a = b ^ (b >>> 9)
|
a = b ^ (b >>> 9)
|
||||||
b = (c + (c << 3)) | 0
|
b = (c + (c << 3)) | 0
|
||||||
c = (c << 21) | (c >>> 11)
|
c = (c << 21) | (c >>> 11)
|
||||||
|
@ -39,7 +40,7 @@ export function createRNG(seed: string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const shuffle = (array: any[], rand: () => number) => {
|
export const shuffle = (array: unknown[], rand: () => number) => {
|
||||||
for (let i = 0; i < array.length; i++) {
|
for (let i = 0; i < array.length; i++) {
|
||||||
const swapIndex = Math.floor(rand() * (array.length - i))
|
const swapIndex = Math.floor(rand() * (array.length - i))
|
||||||
;[array[i], array[swapIndex]] = [array[swapIndex], array[i]]
|
;[array[i], array[swapIndex]] = [array[swapIndex], array[i]]
|
||||||
|
|
|
@ -11,6 +11,7 @@ module.exports = {
|
||||||
plugins: ['@typescript-eslint'],
|
plugins: ['@typescript-eslint'],
|
||||||
parser: '@typescript-eslint/parser',
|
parser: '@typescript-eslint/parser',
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
|
tsconfigRootDir: __dirname,
|
||||||
project: ['./tsconfig.json'],
|
project: ['./tsconfig.json'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -262,7 +262,7 @@ export const sendNewCommentEmail = async (
|
||||||
return
|
return
|
||||||
|
|
||||||
const { question, creatorUsername, slug } = contract
|
const { question, creatorUsername, slug } = contract
|
||||||
const marketUrl = `https://${DOMAIN}/${creatorUsername}/${slug}`
|
const marketUrl = `https://${DOMAIN}/${creatorUsername}/${slug}#${comment.id}`
|
||||||
|
|
||||||
const unsubscribeUrl = `https://us-central1-${PROJECT_ID}.cloudfunctions.net/unsubscribe?id=${userId}&type=market-comment`
|
const unsubscribeUrl = `https://us-central1-${PROJECT_ID}.cloudfunctions.net/unsubscribe?id=${userId}&type=market-comment`
|
||||||
|
|
||||||
|
|
|
@ -1,36 +1,56 @@
|
||||||
import { Row } from './layout/row'
|
import { Row } from './layout/row'
|
||||||
|
import { RadioGroup } from '@headlessui/react'
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
export function ChoicesToggleGroup(props: {
|
export function ChoicesToggleGroup(props: {
|
||||||
currentChoice: number | string
|
currentChoice: number | string
|
||||||
setChoice: (p: any) => void //number | string does not work here because of SetStateAction in .tsx
|
choicesMap: { [key: string]: string | number }
|
||||||
choices: (number | string)[]
|
|
||||||
titles: string[]
|
|
||||||
isSubmitting?: boolean
|
isSubmitting?: boolean
|
||||||
|
setChoice: (p: number | string) => void
|
||||||
|
className?: string
|
||||||
|
children?: React.ReactNode
|
||||||
}) {
|
}) {
|
||||||
const { currentChoice, setChoice, titles, choices, isSubmitting } = props
|
const {
|
||||||
const baseButtonClassName = 'btn btn-outline btn-md sm:btn-md normal-case'
|
currentChoice,
|
||||||
const activeClasss =
|
setChoice,
|
||||||
'bg-indigo-600 focus:bg-indigo-600 hover:bg-indigo-600 text-white'
|
isSubmitting,
|
||||||
|
choicesMap,
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
} = props
|
||||||
return (
|
return (
|
||||||
<Row className={'mt-2 items-center gap-2'}>
|
<Row className={'mt-2 items-center gap-2'}>
|
||||||
<div className={'btn-group justify-stretch'}>
|
<RadioGroup
|
||||||
{choices.map((choice, i) => {
|
value={currentChoice.toString()}
|
||||||
return (
|
onChange={(str) => null}
|
||||||
<button
|
className="mt-2"
|
||||||
key={choice.toString()}
|
>
|
||||||
|
<div className={`grid grid-cols-12 gap-3`}>
|
||||||
|
{Object.keys(choicesMap).map((choiceKey) => (
|
||||||
|
<RadioGroup.Option
|
||||||
|
key={choiceKey}
|
||||||
|
value={choicesMap[choiceKey]}
|
||||||
|
onClick={() => setChoice(choicesMap[choiceKey])}
|
||||||
|
className={({ active }) =>
|
||||||
|
clsx(
|
||||||
|
active ? 'ring-2 ring-indigo-500 ring-offset-2' : '',
|
||||||
|
currentChoice === choicesMap[choiceKey]
|
||||||
|
? 'border-transparent bg-indigo-500 text-white hover:bg-indigo-600'
|
||||||
|
: 'border-gray-200 bg-white text-gray-900 hover:bg-gray-50',
|
||||||
|
'flex cursor-pointer items-center justify-center rounded-md border py-3 px-3 text-sm font-medium normal-case',
|
||||||
|
"hover:ring-offset-2' hover:ring-2 hover:ring-indigo-500",
|
||||||
|
className
|
||||||
|
)
|
||||||
|
}
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
className={clsx(
|
|
||||||
baseButtonClassName,
|
|
||||||
currentChoice === choice ? activeClasss : ''
|
|
||||||
)}
|
|
||||||
onClick={() => setChoice(choice)}
|
|
||||||
>
|
>
|
||||||
{titles[i]}
|
<RadioGroup.Label as="span">{choiceKey}</RadioGroup.Label>
|
||||||
</button>
|
</RadioGroup.Option>
|
||||||
)
|
))}
|
||||||
})}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
</RadioGroup>
|
||||||
</Row>
|
</Row>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -213,7 +213,7 @@ function EditableCloseDate(props: {
|
||||||
|
|
||||||
const [isEditingCloseTime, setIsEditingCloseTime] = useState(false)
|
const [isEditingCloseTime, setIsEditingCloseTime] = useState(false)
|
||||||
const [closeDate, setCloseDate] = useState(
|
const [closeDate, setCloseDate] = useState(
|
||||||
closeTime && dayjs(closeTime).format('YYYY-MM-DDT23:59')
|
closeTime && dayjs(closeTime).format('YYYY-MM-DDTHH:mm')
|
||||||
)
|
)
|
||||||
|
|
||||||
const isSameYear = dayjs(closeTime).isSame(dayjs(), 'year')
|
const isSameYear = dayjs(closeTime).isSame(dayjs(), 'year')
|
||||||
|
|
|
@ -39,16 +39,21 @@ export function QuickBet(props: { contract: Contract }) {
|
||||||
|
|
||||||
const user = useUser()
|
const user = useUser()
|
||||||
const userBets = useUserContractBets(user?.id, contract.id)
|
const userBets = useUserContractBets(user?.id, contract.id)
|
||||||
|
const topAnswer =
|
||||||
|
contract.outcomeType === 'FREE_RESPONSE'
|
||||||
|
? getTopAnswer(contract as FreeResponseContract)
|
||||||
|
: undefined
|
||||||
|
|
||||||
|
// TODO: yes/no from useSaveShares doesn't work on numeric contracts
|
||||||
const { yesFloorShares, noFloorShares } = useSaveShares(
|
const { yesFloorShares, noFloorShares } = useSaveShares(
|
||||||
contract as FullContract<CPMM | DPM, Binary>,
|
contract as FullContract<DPM | CPMM, Binary | FreeResponseContract>,
|
||||||
userBets
|
userBets,
|
||||||
|
topAnswer?.number.toString() || undefined
|
||||||
)
|
)
|
||||||
// TODO: This relies on a hack in useSaveShares, where noFloorShares includes
|
|
||||||
// all non-YES shares. Ideally, useSaveShares should group by all outcomes
|
|
||||||
const hasUpShares =
|
const hasUpShares =
|
||||||
contract.outcomeType === 'BINARY' ? yesFloorShares : noFloorShares
|
yesFloorShares || (noFloorShares && contract.outcomeType === 'NUMERIC')
|
||||||
const hasDownShares =
|
const hasDownShares =
|
||||||
contract.outcomeType === 'BINARY' ? noFloorShares : yesFloorShares
|
noFloorShares && yesFloorShares <= 0 && contract.outcomeType !== 'NUMERIC'
|
||||||
|
|
||||||
const [upHover, setUpHover] = useState(false)
|
const [upHover, setUpHover] = useState(false)
|
||||||
const [downHover, setDownHover] = useState(false)
|
const [downHover, setDownHover] = useState(false)
|
||||||
|
|
|
@ -21,14 +21,11 @@ export function CopyLinkDateTimeComponent(props: {
|
||||||
event: React.MouseEvent<HTMLAnchorElement, MouseEvent>
|
event: React.MouseEvent<HTMLAnchorElement, MouseEvent>
|
||||||
) {
|
) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
|
let elementLocation = `https://${ENV_CONFIG.domain}${contractPath(
|
||||||
|
contract
|
||||||
|
)}#${elementId}`
|
||||||
|
|
||||||
let currentLocation = window.location.href.includes('/home')
|
copyToClipboard(elementLocation)
|
||||||
? `https://${ENV_CONFIG.domain}${contractPath(contract)}#${elementId}`
|
|
||||||
: window.location.href
|
|
||||||
if (currentLocation.includes('#')) {
|
|
||||||
currentLocation = currentLocation.split('#')[0]
|
|
||||||
}
|
|
||||||
copyToClipboard(`${currentLocation}#${elementId}`)
|
|
||||||
setShowToast(true)
|
setShowToast(true)
|
||||||
setTimeout(() => setShowToast(false), 2000)
|
setTimeout(() => setShowToast(false), 2000)
|
||||||
}
|
}
|
||||||
|
|
|
@ -156,7 +156,7 @@ export function FeedComment(props: {
|
||||||
<Row
|
<Row
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'flex space-x-1.5 transition-all duration-1000 sm:space-x-3',
|
'flex space-x-1.5 transition-all duration-1000 sm:space-x-3',
|
||||||
highlighted ? `-m-2 rounded bg-indigo-500/[0.2] p-2` : ''
|
highlighted ? `-m-1 rounded bg-indigo-500/[0.2] p-2` : ''
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Avatar
|
<Avatar
|
||||||
|
|
|
@ -127,13 +127,10 @@ export default function Sidebar(props: { className?: string }) {
|
||||||
const currentPage = router.pathname
|
const currentPage = router.pathname
|
||||||
const [countdown, setCountdown] = useState('...')
|
const [countdown, setCountdown] = useState('...')
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const utcMidnightToLocalDate = new Date(getUtcFreeMarketResetTime(false))
|
const nextUtcResetTime = getUtcFreeMarketResetTime(false)
|
||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
const now = new Date().getTime()
|
const now = new Date().getTime()
|
||||||
let timeUntil = Math.abs(utcMidnightToLocalDate.getTime() - now)
|
let timeUntil = nextUtcResetTime - now
|
||||||
if (now > utcMidnightToLocalDate.getTime()) {
|
|
||||||
timeUntil = 24 * 60 * 60 * 1000 - timeUntil
|
|
||||||
}
|
|
||||||
const hoursUntil = timeUntil / 1000 / 60 / 60
|
const hoursUntil = timeUntil / 1000 / 60 / 60
|
||||||
const minutesUntil = Math.floor((hoursUntil * 60) % 60)
|
const minutesUntil = Math.floor((hoursUntil * 60) % 60)
|
||||||
const secondsUntil = Math.floor((hoursUntil * 60 * 60) % 60)
|
const secondsUntil = Math.floor((hoursUntil * 60 * 60) % 60)
|
||||||
|
|
|
@ -1,11 +1,19 @@
|
||||||
import { Binary, CPMM, DPM, FullContract } from 'common/contract'
|
import {
|
||||||
|
Binary,
|
||||||
|
CPMM,
|
||||||
|
DPM,
|
||||||
|
FreeResponseContract,
|
||||||
|
FullContract,
|
||||||
|
} from 'common/contract'
|
||||||
import { Bet } from 'common/bet'
|
import { Bet } from 'common/bet'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { partition, sumBy } from 'lodash'
|
import { partition, sumBy } from 'lodash'
|
||||||
|
import { safeLocalStorage } from 'web/lib/util/local'
|
||||||
|
|
||||||
export const useSaveShares = (
|
export const useSaveShares = (
|
||||||
contract: FullContract<CPMM | DPM, Binary>,
|
contract: FullContract<CPMM | DPM, Binary | FreeResponseContract>,
|
||||||
userBets: Bet[] | undefined
|
userBets: Bet[] | undefined,
|
||||||
|
freeResponseAnswerOutcome?: string
|
||||||
) => {
|
) => {
|
||||||
const [savedShares, setSavedShares] = useState<
|
const [savedShares, setSavedShares] = useState<
|
||||||
| {
|
| {
|
||||||
|
@ -17,9 +25,11 @@ export const useSaveShares = (
|
||||||
| undefined
|
| undefined
|
||||||
>()
|
>()
|
||||||
|
|
||||||
const [yesBets, noBets] = partition(
|
// TODO: How do we handle numeric yes / no bets? - maybe bet amounts above vs below the highest peak
|
||||||
userBets ?? [],
|
const [yesBets, noBets] = partition(userBets ?? [], (bet) =>
|
||||||
(bet) => bet.outcome === 'YES'
|
freeResponseAnswerOutcome
|
||||||
|
? bet.outcome === freeResponseAnswerOutcome
|
||||||
|
: bet.outcome === 'YES'
|
||||||
)
|
)
|
||||||
const [yesShares, noShares] = [
|
const [yesShares, noShares] = [
|
||||||
sumBy(yesBets, (bet) => bet.shares),
|
sumBy(yesBets, (bet) => bet.shares),
|
||||||
|
@ -30,18 +40,16 @@ export const useSaveShares = (
|
||||||
const noFloorShares = Math.round(noShares) === 0 ? 0 : Math.floor(noShares)
|
const noFloorShares = Math.round(noShares) === 0 ? 0 : Math.floor(noShares)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const local = safeLocalStorage()
|
||||||
// Save yes and no shares to local storage.
|
// Save yes and no shares to local storage.
|
||||||
const savedShares = localStorage.getItem(`${contract.id}-shares`)
|
const savedShares = local?.getItem(`${contract.id}-shares`)
|
||||||
if (!userBets && savedShares) {
|
if (!userBets && savedShares) {
|
||||||
setSavedShares(JSON.parse(savedShares))
|
setSavedShares(JSON.parse(savedShares))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userBets) {
|
if (userBets) {
|
||||||
const updatedShares = { yesShares, noShares }
|
const updatedShares = { yesShares, noShares }
|
||||||
localStorage.setItem(
|
local?.setItem(`${contract.id}-shares`, JSON.stringify(updatedShares))
|
||||||
`${contract.id}-shares`,
|
|
||||||
JSON.stringify(updatedShares)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}, [contract.id, userBets, noShares, yesShares])
|
}, [contract.id, userBets, noShares, yesShares])
|
||||||
|
|
||||||
|
|
|
@ -1,17 +1,36 @@
|
||||||
import { listContracts } from 'web/lib/firebase/contracts'
|
import { listContracts } from 'web/lib/firebase/contracts'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { User } from 'common/user'
|
import { User } from 'common/user'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
import utc from 'dayjs/plugin/utc'
|
||||||
|
dayjs.extend(utc)
|
||||||
|
|
||||||
let sessionCreatedContractToday = true
|
let sessionCreatedContractToday = true
|
||||||
|
|
||||||
export function getUtcFreeMarketResetTime(yesterday: boolean) {
|
export function getUtcFreeMarketResetTime(previous: boolean) {
|
||||||
// Uses utc time like the server.
|
const localTimeNow = new Date()
|
||||||
const utcFreeMarketResetTime = new Date()
|
const utc4pmToday = dayjs()
|
||||||
utcFreeMarketResetTime.setUTCDate(
|
.utc()
|
||||||
utcFreeMarketResetTime.getUTCDate() - (yesterday ? 1 : 0)
|
.set('hour', 16)
|
||||||
)
|
.set('minute', 0)
|
||||||
const utcFreeMarketMS = utcFreeMarketResetTime.setUTCHours(16, 0, 0, 0)
|
.set('second', 0)
|
||||||
return utcFreeMarketMS
|
.set('millisecond', 0)
|
||||||
|
|
||||||
|
// if it's after 4pm UTC today
|
||||||
|
if (localTimeNow.getTime() > utc4pmToday.valueOf()) {
|
||||||
|
return previous
|
||||||
|
? // Return it as it is
|
||||||
|
utc4pmToday.valueOf()
|
||||||
|
: // Or add 24 hours to get the next 4pm UTC time:
|
||||||
|
utc4pmToday.valueOf() + 24 * 60 * 60 * 1000
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4pm UTC today is coming up
|
||||||
|
return previous
|
||||||
|
? // Subtract 24 hours to get the previous 4pm UTC time:
|
||||||
|
utc4pmToday.valueOf() - 24 * 60 * 60 * 1000
|
||||||
|
: // Return it as it is
|
||||||
|
utc4pmToday.valueOf()
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useHasCreatedContractToday = (user: User | null | undefined) => {
|
export const useHasCreatedContractToday = (user: User | null | undefined) => {
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { User } from 'common/user'
|
||||||
import { randomString } from 'common/util/random'
|
import { randomString } from 'common/util/random'
|
||||||
import './init'
|
import './init'
|
||||||
import { functions } from './init'
|
import { functions } from './init'
|
||||||
|
import { safeLocalStorage } from '../util/local'
|
||||||
|
|
||||||
export const cloudFunction = <RequestData, ResponseData>(name: string) =>
|
export const cloudFunction = <RequestData, ResponseData>(name: string) =>
|
||||||
httpsCallable<RequestData, ResponseData>(functions, name)
|
httpsCallable<RequestData, ResponseData>(functions, name)
|
||||||
|
@ -48,10 +49,11 @@ export const resolveMarket = cloudFunction<
|
||||||
>('resolveMarket')
|
>('resolveMarket')
|
||||||
|
|
||||||
export const createUser: () => Promise<User | null> = () => {
|
export const createUser: () => Promise<User | null> = () => {
|
||||||
let deviceToken = window.localStorage.getItem('device-token')
|
const local = safeLocalStorage()
|
||||||
|
let deviceToken = local?.getItem('device-token')
|
||||||
if (!deviceToken) {
|
if (!deviceToken) {
|
||||||
deviceToken = randomString()
|
deviceToken = randomString()
|
||||||
window.localStorage.setItem('device-token', deviceToken)
|
local?.setItem('device-token', deviceToken)
|
||||||
}
|
}
|
||||||
|
|
||||||
return cloudFunction('createUser')({ deviceToken })
|
return cloudFunction('createUser')({ deviceToken })
|
||||||
|
|
|
@ -27,6 +27,7 @@ import { getValue, getValues, listenForValue, listenForValues } from './utils'
|
||||||
import { DAY_MS } from 'common/util/time'
|
import { DAY_MS } from 'common/util/time'
|
||||||
import { feed } from 'common/feed'
|
import { feed } from 'common/feed'
|
||||||
import { CATEGORY_LIST } from 'common/categories'
|
import { CATEGORY_LIST } from 'common/categories'
|
||||||
|
import { safeLocalStorage } from '../util/local'
|
||||||
|
|
||||||
export type { User }
|
export type { User }
|
||||||
|
|
||||||
|
@ -86,8 +87,9 @@ let createUserPromise: Promise<User | null> | undefined = undefined
|
||||||
const warmUpCreateUser = throttle(createUser, 5000 /* ms */)
|
const warmUpCreateUser = throttle(createUser, 5000 /* ms */)
|
||||||
|
|
||||||
export function listenForLogin(onUser: (user: User | null) => void) {
|
export function listenForLogin(onUser: (user: User | null) => void) {
|
||||||
const cachedUser = localStorage.getItem(CACHED_USER_KEY)
|
const local = safeLocalStorage()
|
||||||
onUser(cachedUser ? JSON.parse(cachedUser) : null)
|
const cachedUser = local?.getItem(CACHED_USER_KEY)
|
||||||
|
onUser(cachedUser && JSON.parse(cachedUser))
|
||||||
|
|
||||||
if (!cachedUser) warmUpCreateUser()
|
if (!cachedUser) warmUpCreateUser()
|
||||||
|
|
||||||
|
@ -106,11 +108,11 @@ export function listenForLogin(onUser: (user: User | null) => void) {
|
||||||
|
|
||||||
// Persist to local storage, to reduce login blink next time.
|
// Persist to local storage, to reduce login blink next time.
|
||||||
// Note: Cap on localStorage size is ~5mb
|
// Note: Cap on localStorage size is ~5mb
|
||||||
localStorage.setItem(CACHED_USER_KEY, JSON.stringify(user))
|
local?.setItem(CACHED_USER_KEY, JSON.stringify(user))
|
||||||
} else {
|
} else {
|
||||||
// User logged out; reset to null
|
// User logged out; reset to null
|
||||||
onUser(null)
|
onUser(null)
|
||||||
localStorage.removeItem(CACHED_USER_KEY)
|
local?.removeItem(CACHED_USER_KEY)
|
||||||
createUserPromise = undefined
|
createUserPromise = undefined
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
11
web/lib/util/local.ts
Normal file
11
web/lib/util/local.ts
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
export const safeLocalStorage = () => (isLocalStorage() ? localStorage : null)
|
||||||
|
|
||||||
|
const isLocalStorage = () => {
|
||||||
|
try {
|
||||||
|
localStorage.getItem('test')
|
||||||
|
localStorage.setItem('hi', 'mom')
|
||||||
|
return true
|
||||||
|
} catch (e) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,7 +18,7 @@ import { getDailyNewUsers } from 'web/lib/firebase/users'
|
||||||
|
|
||||||
export const getStaticProps = fromPropz(getStaticPropz)
|
export const getStaticProps = fromPropz(getStaticPropz)
|
||||||
export async function getStaticPropz() {
|
export async function getStaticPropz() {
|
||||||
const numberOfDays = 45
|
const numberOfDays = 90
|
||||||
const today = dayjs(dayjs().format('YYYY-MM-DD'))
|
const today = dayjs(dayjs().format('YYYY-MM-DD'))
|
||||||
// Convert from UTC midnight to PT midnight.
|
// Convert from UTC midnight to PT midnight.
|
||||||
.add(7, 'hours')
|
.add(7, 'hours')
|
||||||
|
|
|
@ -17,8 +17,6 @@ import { useHasCreatedContractToday } from 'web/hooks/use-has-created-contract-t
|
||||||
import { removeUndefinedProps } from 'common/util/object'
|
import { removeUndefinedProps } from 'common/util/object'
|
||||||
import { CATEGORIES } from 'common/categories'
|
import { CATEGORIES } from 'common/categories'
|
||||||
import { ChoicesToggleGroup } from 'web/components/choices-toggle-group'
|
import { ChoicesToggleGroup } from 'web/components/choices-toggle-group'
|
||||||
import { CalculatorIcon } from '@heroicons/react/outline'
|
|
||||||
import { write } from 'fs'
|
|
||||||
|
|
||||||
export default function Create() {
|
export default function Create() {
|
||||||
const [question, setQuestion] = useState('')
|
const [question, setQuestion] = useState('')
|
||||||
|
@ -71,8 +69,6 @@ export function NewContract(props: { question: string; tag?: string }) {
|
||||||
const [minString, setMinString] = useState('')
|
const [minString, setMinString] = useState('')
|
||||||
const [maxString, setMaxString] = useState('')
|
const [maxString, setMaxString] = useState('')
|
||||||
const [description, setDescription] = useState('')
|
const [description, setDescription] = useState('')
|
||||||
const [showCalendar, setShowCalendar] = useState(false)
|
|
||||||
const [showNumInput, setShowNumInput] = useState(false)
|
|
||||||
|
|
||||||
const [category, setCategory] = useState<string>('')
|
const [category, setCategory] = useState<string>('')
|
||||||
// const [tagText, setTagText] = useState<string>(tag ?? '')
|
// const [tagText, setTagText] = useState<string>(tag ?? '')
|
||||||
|
@ -93,20 +89,25 @@ export function NewContract(props: { question: string; tag?: string }) {
|
||||||
// By default, close the market a week from today
|
// By default, close the market a week from today
|
||||||
const [closeDate, setCloseDate] = useState<undefined | string>(weekFrom(dayjs()))
|
const [closeDate, setCloseDate] = useState<undefined | string>(weekFrom(dayjs()))
|
||||||
const [resolutionDate, setResolutionDate] = useState<undefined | string>(weekFrom(closeDate))
|
const [resolutionDate, setResolutionDate] = useState<undefined | string>(weekFrom(closeDate))
|
||||||
|
const [closeHoursMinutes, setCloseHoursMinutes] = useState<string>('23:59')
|
||||||
|
const [probErrorText, setProbErrorText] = useState('')
|
||||||
|
const [marketInfoText, setMarketInfoText] = useState('')
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||||
|
|
||||||
const closeTime = closeDate ? dayjs(closeDate).valueOf() : undefined
|
const closeTime = closeDate
|
||||||
|
? dayjs(`${closeDate}T${closeHoursMinutes}`).valueOf()
|
||||||
|
: undefined
|
||||||
const automaticResolutionTime = resolutionDate ? dayjs(resolutionDate).valueOf() : undefined
|
const automaticResolutionTime = resolutionDate ? dayjs(resolutionDate).valueOf() : undefined
|
||||||
|
|
||||||
const balance = creator?.balance || 0
|
const balance = creator?.balance || 0
|
||||||
|
|
||||||
const min = minString ? parseFloat(minString) : undefined
|
const min = minString ? parseFloat(minString) : undefined
|
||||||
const max = maxString ? parseFloat(maxString) : undefined
|
const max = maxString ? parseFloat(maxString) : undefined
|
||||||
|
// get days from today until the end of this year:
|
||||||
|
const daysLeftInTheYear = dayjs().endOf('year').diff(dayjs(), 'day')
|
||||||
|
|
||||||
const isValid =
|
const isValid =
|
||||||
initialProb > 0 &&
|
(outcomeType === 'BINARY' ? initialProb >= 5 && initialProb <= 95 : true) &&
|
||||||
initialProb < 100 &&
|
|
||||||
question.length > 0 &&
|
question.length > 0 &&
|
||||||
ante !== undefined &&
|
ante !== undefined &&
|
||||||
ante !== null &&
|
ante !== null &&
|
||||||
|
@ -130,8 +131,7 @@ export function NewContract(props: { question: string; tag?: string }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function setCloseDateInDays(days: number) {
|
function setCloseDateInDays(days: number) {
|
||||||
setShowCalendar(days === 0)
|
const newCloseDate = dayjs().add(days, 'day').format('YYYY-MM-DD')
|
||||||
const newCloseDate = dayjs().add(days, 'day').format('YYYY-MM-DDT23:59')
|
|
||||||
setCloseDate(newCloseDate)
|
setCloseDate(newCloseDate)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -173,90 +173,83 @@ export function NewContract(props: { question: string; tag?: string }) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<label className="label mt-1">
|
<label className="label mt-1">
|
||||||
<span className="my-1">Answer type</span>
|
<span className="mt-1">Answer type</span>
|
||||||
</label>
|
</label>
|
||||||
<Row className="form-control gap-2">
|
<ChoicesToggleGroup
|
||||||
<label className="label cursor-pointer gap-2">
|
currentChoice={outcomeType}
|
||||||
<input
|
setChoice={(choice) => {
|
||||||
className="radio"
|
if (choice === 'NUMERIC')
|
||||||
type="radio"
|
setMarketInfoText(
|
||||||
name="opt"
|
'Numeric markets are still experimental and subject to major revisions.'
|
||||||
checked={outcomeType === 'BINARY'}
|
)
|
||||||
value="BINARY"
|
else if (choice === 'FREE_RESPONSE')
|
||||||
onChange={() => setOutcomeType('BINARY')}
|
setMarketInfoText(
|
||||||
disabled={isSubmitting}
|
'Users can submit their own answers to this market.'
|
||||||
/>
|
)
|
||||||
<span className="label-text">Yes / No</span>
|
else setMarketInfoText('')
|
||||||
</label>
|
setOutcomeType(choice as outcomeType)
|
||||||
|
}}
|
||||||
<label className="label cursor-pointer gap-2">
|
choicesMap={{
|
||||||
<input
|
'Yes / No': 'BINARY',
|
||||||
className="radio"
|
'Free response': 'FREE_RESPONSE',
|
||||||
type="radio"
|
Numeric: 'NUMERIC',
|
||||||
name="opt"
|
}}
|
||||||
checked={outcomeType === 'FREE_RESPONSE'}
|
isSubmitting={isSubmitting}
|
||||||
value="FREE_RESPONSE"
|
className={'col-span-4'}
|
||||||
onChange={() => setOutcomeType('FREE_RESPONSE')}
|
/>
|
||||||
disabled={isSubmitting}
|
{marketInfoText && (
|
||||||
/>
|
<div className="mt-2 ml-1 text-sm text-indigo-700">
|
||||||
<span className="label-text">Free response</span>
|
{marketInfoText}
|
||||||
</label>
|
</div>
|
||||||
<label className="label cursor-pointer gap-2">
|
)}
|
||||||
<input
|
|
||||||
className="radio"
|
|
||||||
type="radio"
|
|
||||||
name="opt"
|
|
||||||
checked={outcomeType === 'NUMERIC'}
|
|
||||||
value="NUMERIC"
|
|
||||||
onChange={() => setOutcomeType('NUMERIC')}
|
|
||||||
/>
|
|
||||||
<span className="label-text">Numeric (experimental)</span>
|
|
||||||
</label>
|
|
||||||
</Row>
|
|
||||||
<Spacer h={4} />
|
<Spacer h={4} />
|
||||||
|
|
||||||
{outcomeType === 'BINARY' && (
|
{outcomeType === 'BINARY' && (
|
||||||
<div className="form-control">
|
<div className="form-control">
|
||||||
<Row className="label justify-start">
|
<Row className="label justify-start">
|
||||||
<span className="mb-1">How likely is it to happen?</span>
|
<span className="mb-1">How likely is it to happen?</span>
|
||||||
<CalculatorIcon
|
|
||||||
className={clsx(
|
|
||||||
'ml-2 cursor-pointer rounded-md',
|
|
||||||
'hover:bg-gray-200',
|
|
||||||
showNumInput && 'stroke-indigo-700'
|
|
||||||
)}
|
|
||||||
height={20}
|
|
||||||
onClick={() => setShowNumInput(!showNumInput)}
|
|
||||||
/>
|
|
||||||
</Row>
|
</Row>
|
||||||
<Row className={'w-full items-center sm:gap-2'}>
|
<Row className={'justify-start'}>
|
||||||
<ChoicesToggleGroup
|
<ChoicesToggleGroup
|
||||||
currentChoice={initialProb}
|
currentChoice={initialProb}
|
||||||
setChoice={setInitialProb}
|
setChoice={(option) => {
|
||||||
choices={[25, 50, 75]}
|
setProbErrorText('')
|
||||||
titles={['Unlikely', 'Unsure', 'Likely']}
|
setInitialProb(option as number)
|
||||||
|
}}
|
||||||
|
choicesMap={{
|
||||||
|
Unlikely: 25,
|
||||||
|
'Not Sure': 50,
|
||||||
|
Likely: 75,
|
||||||
|
}}
|
||||||
isSubmitting={isSubmitting}
|
isSubmitting={isSubmitting}
|
||||||
/>
|
className={'col-span-4 sm:col-span-3'}
|
||||||
{showNumInput && (
|
>
|
||||||
<>
|
<Row className={'col-span-3 items-center justify-start'}>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
value={initialProb}
|
value={initialProb}
|
||||||
className={
|
className={
|
||||||
'max-w-[16%] sm:max-w-[15%] ' +
|
'input-bordered input-md max-w-[100px] rounded-md border-gray-300 pr-2 text-lg'
|
||||||
'input-bordered input-md mt-2 rounded-md p-1 text-lg sm:p-4'
|
|
||||||
}
|
}
|
||||||
|
min={5}
|
||||||
|
max={95}
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
min={10}
|
onChange={(e) => {
|
||||||
max={90}
|
// show error if prob is less than 5 or greater than 95:
|
||||||
onChange={(e) =>
|
const prob = parseInt(e.target.value)
|
||||||
setInitialProb(parseInt(e.target.value.substring(0, 2)))
|
setInitialProb(prob)
|
||||||
}
|
if (prob < 5 || prob > 95)
|
||||||
|
setProbErrorText('Probability must be between 5% and 95%')
|
||||||
|
else setProbErrorText('')
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<span className={'mt-2'}>%</span>
|
<span className={'ml-1'}>%</span>
|
||||||
</>
|
</Row>
|
||||||
)}
|
</ChoicesToggleGroup>
|
||||||
</Row>
|
</Row>
|
||||||
|
{probErrorText && (
|
||||||
|
<div className="text-error mt-2 ml-1 text-sm">{probErrorText}</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
@ -299,7 +292,7 @@ export function NewContract(props: { question: string; tag?: string }) {
|
||||||
<div className="form-control mb-1 items-start">
|
<div className="form-control mb-1 items-start">
|
||||||
<label className="label mb-1 gap-2">
|
<label className="label mb-1 gap-2">
|
||||||
<span className="mb-1">Description</span>
|
<span className="mb-1">Description</span>
|
||||||
<InfoTooltip text="Optional. Describe how you will resolve this market." />
|
<InfoTooltip text="Optional. Describe how you will resolve this question." />
|
||||||
</label>
|
</label>
|
||||||
<Textarea
|
<Textarea
|
||||||
className="textarea textarea-bordered w-full resize-none"
|
className="textarea textarea-bordered w-full resize-none"
|
||||||
|
@ -338,41 +331,47 @@ export function NewContract(props: { question: string; tag?: string }) {
|
||||||
|
|
||||||
<div className="form-control mb-1 items-start">
|
<div className="form-control mb-1 items-start">
|
||||||
<label className="label mb-1 gap-2">
|
<label className="label mb-1 gap-2">
|
||||||
<span>Question expires in a:</span>
|
<span>Question closes in:</span>
|
||||||
<InfoTooltip text="Betting will be halted after this date (local timezone)." />
|
<InfoTooltip text="Betting will be halted after this time (local timezone)." />
|
||||||
</label>
|
</label>
|
||||||
<Row className={'w-full items-center gap-2'}>
|
<Row className={'w-full items-center gap-2'}>
|
||||||
<ChoicesToggleGroup
|
<ChoicesToggleGroup
|
||||||
currentChoice={
|
currentChoice={dayjs(`${closeDate}T23:59`).diff(dayjs(), 'day')}
|
||||||
closeDate
|
setChoice={(choice) => {
|
||||||
? [1, 7, 30, 365, 0].includes(
|
setCloseDateInDays(choice as number)
|
||||||
dayjs(closeDate).diff(dayjs(), 'day')
|
}}
|
||||||
)
|
choicesMap={{
|
||||||
? dayjs(closeDate).diff(dayjs(), 'day')
|
'A day': 1,
|
||||||
: 0
|
'A week': 7,
|
||||||
: -1
|
'30 days': 30,
|
||||||
}
|
'This year': daysLeftInTheYear,
|
||||||
setChoice={setCloseDateInDays}
|
}}
|
||||||
choices={[1, 7, 30, 0]}
|
|
||||||
titles={['Day', 'Week', 'Month', 'Custom']}
|
|
||||||
isSubmitting={isSubmitting}
|
isSubmitting={isSubmitting}
|
||||||
|
className={'col-span-4 sm:col-span-2'}
|
||||||
/>
|
/>
|
||||||
</Row>
|
</Row>
|
||||||
{showCalendar && (
|
<Row>
|
||||||
<input
|
<input
|
||||||
type={'date'}
|
type={'date'}
|
||||||
className="input input-bordered mt-4"
|
className="input input-bordered mt-4"
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setCloseDate(
|
setCloseDate(dayjs(e.target.value).format('YYYY-MM-DD') || '')
|
||||||
dayjs(e.target.value).format('YYYY-MM-DDT23:59') || ''
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
min={Date.now()}
|
min={Date.now()}
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
value={dayjs(closeDate).format('YYYY-MM-DD')}
|
value={dayjs(closeDate).format('YYYY-MM-DD')}
|
||||||
/>
|
/>
|
||||||
)}
|
<input
|
||||||
|
type={'time'}
|
||||||
|
className="input input-bordered mt-4 ml-2"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
onChange={(e) => setCloseHoursMinutes(e.target.value)}
|
||||||
|
min={'00:00'}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
value={closeHoursMinutes}
|
||||||
|
/>
|
||||||
|
</Row>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{outcomeType === 'BINARY' && (
|
{outcomeType === 'BINARY' && (
|
||||||
|
@ -413,18 +412,23 @@ export function NewContract(props: { question: string; tag?: string }) {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{resolutionType === 'COMBINED' && (
|
{resolutionType === 'COMBINED' && (
|
||||||
<div className="form-control mb-1 items-start">
|
<div className="form-control">
|
||||||
<label className="label mb-1 gap-2">
|
<Row className="label justify-start">
|
||||||
<span>Question resolves automatically as:</span>
|
<span>Question resolves automatically as:</span>
|
||||||
<InfoTooltip text="The market will be resolved automatically on this date (local timezone)." />
|
<InfoTooltip text="The market will be resolved automatically on this date (local timezone)." />
|
||||||
</label>
|
</Row>
|
||||||
<Row className={'w-full items-center gap-2'}>
|
<Row className={'justify-start'}>
|
||||||
<ChoicesToggleGroup
|
<ChoicesToggleGroup
|
||||||
currentChoice={automaticResolution}
|
currentChoice={automaticResolution}
|
||||||
setChoice={setAutomaticResolution}
|
setChoice={(choice) => setAutomaticResolution(choice as resolution)}
|
||||||
choices={RESOLUTIONS}
|
choicesMap={{
|
||||||
titles={['YES', 'NO', 'PROB', 'N/A']}
|
'YES': 'YES',
|
||||||
|
'NO': 'NO',
|
||||||
|
'MKT': 'PROB',
|
||||||
|
'CANCEL': 'N/A',
|
||||||
|
}}
|
||||||
isSubmitting={isSubmitting}
|
isSubmitting={isSubmitting}
|
||||||
|
className={'col-span-4 sm:col-span-3'}
|
||||||
/>
|
/>
|
||||||
</Row>
|
</Row>
|
||||||
<input
|
<input
|
||||||
|
@ -453,7 +457,7 @@ export function NewContract(props: { question: string; tag?: string }) {
|
||||||
{mustWaitForDailyFreeMarketStatus != 'loading' &&
|
{mustWaitForDailyFreeMarketStatus != 'loading' &&
|
||||||
mustWaitForDailyFreeMarketStatus && (
|
mustWaitForDailyFreeMarketStatus && (
|
||||||
<InfoTooltip
|
<InfoTooltip
|
||||||
text={`Cost to create your market. This amount is used to subsidize trading.`}
|
text={`Cost to create your question. This amount is used to subsidize betting.`}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</label>
|
</label>
|
||||||
|
@ -512,7 +516,7 @@ export function NewContract(props: { question: string; tag?: string }) {
|
||||||
submit()
|
submit()
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{isSubmitting ? 'Creating...' : 'Create market'}
|
{isSubmitting ? 'Creating...' : 'Create question'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -54,7 +54,7 @@ export async function getStaticPropz(props: { params: { slugs: string[] } }) {
|
||||||
)
|
)
|
||||||
activeContracts = [...unresolved, ...resolved]
|
activeContracts = [...unresolved, ...resolved]
|
||||||
|
|
||||||
const creatorScores = scoreCreators(contracts, bets)
|
const creatorScores = scoreCreators(contracts)
|
||||||
const traderScores = scoreTraders(contracts, bets)
|
const traderScores = scoreTraders(contracts, bets)
|
||||||
const [topCreators, topTraders] = await Promise.all([
|
const [topCreators, topTraders] = await Promise.all([
|
||||||
toTopUsers(creatorScores),
|
toTopUsers(creatorScores),
|
||||||
|
|
Loading…
Reference in New Issue
Block a user