Merge branch 'main' into automated-market-resolution

# Conflicts:
#	web/components/choices-toggle-group.tsx
#	web/pages/create.tsx
This commit is contained in:
Milli 2022-05-27 23:11:22 +02:00
commit f0dc00e6ad
24 changed files with 277 additions and 197 deletions

View File

@ -9,11 +9,18 @@ module.exports = {
{
files: ['**/*.ts'],
plugins: ['@typescript-eslint'],
extends: ['plugin:@typescript-eslint/recommended'],
parser: '@typescript-eslint/parser',
parserOptions: {
tsconfigRootDir: __dirname,
project: ['./tsconfig.json'],
},
rules: {
'@typescript-eslint/no-explicit-any': 'off',
},
},
],
rules: {
'no-unused-vars': 'off',
'no-constant-condition': ['error', { checkLoops: false }],
'lodash/import-scope': [2, 'member'],
},

View File

@ -170,7 +170,7 @@ export function calculateNumericDpmShares(
([amount]) => amount
).map(([, i]) => i)
for (let i of order) {
for (const i of order) {
const [bucket, bet] = bets[i]
shares[i] = calculateDpmShares(totalShares, bet, bucket)
totalShares = addObjects(totalShares, { [bucket]: shares[i] })

View File

@ -5,13 +5,13 @@ import { THEOREMONE_CONFIG } from './theoremone'
export const ENV = process.env.NEXT_PUBLIC_FIREBASE_ENV ?? 'PROD'
const CONFIGS = {
const CONFIGS: { [env: string]: EnvConfig } = {
PROD: PROD_CONFIG,
DEV: DEV_CONFIG,
THEOREMONE: THEOREMONE_CONFIG,
}
// @ts-ignore
export const ENV_CONFIG: EnvConfig = CONFIGS[ENV]
export const ENV_CONFIG = CONFIGS[ENV]
export function isWhitelisted(email?: string) {
if (!ENV_CONFIG.whitelistEmail) {

View File

@ -1,5 +1,4 @@
import { range } from 'lodash'
import { PHANTOM_ANTE } from './antes'
import {
Binary,
Contract,
@ -14,7 +13,6 @@ import {
import { User } from './user'
import { parseTags } from './util/parse'
import { removeUndefinedProps } from './util/object'
import { calcDpmInitialPool } from './calculate-dpm'
export function getNewContract(
id: string,
@ -86,6 +84,9 @@ export function getNewContract(
return contract as Contract
}
/*
import { PHANTOM_ANTE } from './antes'
import { calcDpmInitialPool } from './calculate-dpm'
const getBinaryDpmProps = (initialProb: number, ante: number) => {
const { sharesYes, sharesNo, poolYes, poolNo, phantomYes, phantomNo } =
calcDpmInitialPool(initialProb, ante, PHANTOM_ANTE)
@ -102,6 +103,7 @@ const getBinaryDpmProps = (initialProb: number, ante: number) => {
return system
}
*/
const getBinaryCpmmProps = (initialProb: number, ante: number) => {
const pool = { YES: ante, NO: ante }
@ -162,11 +164,3 @@ const getNumericProps = (
return system
}
const getMultiProps = (
outcomes: string[],
initialProbs: number[],
ante: number
) => {
// Not implemented.
}

View File

@ -4,7 +4,7 @@ import { Bet } from './bet'
import { Binary, Contract, FullContract } from './contract'
import { getPayouts } from './payouts'
export function scoreCreators(contracts: Contract[], bets: Bet[][]) {
export function scoreCreators(contracts: Contract[]) {
const creatorScore = mapValues(
groupBy(contracts, ({ creatorId }) => creatorId),
(contracts) => sumBy(contracts, ({ pool }) => pool.YES + pool.NO)

12
common/tsconfig.json Normal file
View File

@ -0,0 +1,12 @@
{
"compilerOptions": {
"baseUrl": "../",
"moduleResolution": "node",
"noImplicitReturns": true,
"outDir": "lib",
"sourceMap": true,
"strict": true,
"target": "es2017"
},
"include": ["**/*.ts"]
}

View File

@ -1,9 +1,9 @@
import { union } from 'lodash'
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]
}
@ -17,7 +17,7 @@ export const addObjects = <T extends { [key: string]: number }>(
const keys = union(Object.keys(obj1), Object.keys(obj2))
const newObj = {} as any
for (let key of keys) {
for (const key of keys) {
newObj[key] = (obj1[key] ?? 0) + (obj2[key] ?? 0)
}

View File

@ -5,7 +5,8 @@ export const randomString = (length = 12) =>
export function genHash(str: string) {
// 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 = (h << 13) | (h >>> 19)
}
@ -28,7 +29,7 @@ export function createRNG(seed: string) {
b >>>= 0
c >>>= 0
d >>>= 0
var t = (a + b) | 0
let t = (a + b) | 0
a = b ^ (b >>> 9)
b = (c + (c << 3)) | 0
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++) {
const swapIndex = Math.floor(rand() * (array.length - i))
;[array[i], array[swapIndex]] = [array[swapIndex], array[i]]

View File

@ -11,6 +11,7 @@ module.exports = {
plugins: ['@typescript-eslint'],
parser: '@typescript-eslint/parser',
parserOptions: {
tsconfigRootDir: __dirname,
project: ['./tsconfig.json'],
},
},

View File

@ -262,7 +262,7 @@ export const sendNewCommentEmail = async (
return
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`

View File

@ -1,36 +1,56 @@
import { Row } from './layout/row'
import { RadioGroup } from '@headlessui/react'
import clsx from 'clsx'
import React from 'react'
export function ChoicesToggleGroup(props: {
currentChoice: number | string
setChoice: (p: any) => void //number | string does not work here because of SetStateAction in .tsx
choices: (number | string)[]
titles: string[]
choicesMap: { [key: string]: string | number }
isSubmitting?: boolean
setChoice: (p: number | string) => void
className?: string
children?: React.ReactNode
}) {
const { currentChoice, setChoice, titles, choices, isSubmitting } = props
const baseButtonClassName = 'btn btn-outline btn-md sm:btn-md normal-case'
const activeClasss =
'bg-indigo-600 focus:bg-indigo-600 hover:bg-indigo-600 text-white'
const {
currentChoice,
setChoice,
isSubmitting,
choicesMap,
className,
children,
} = props
return (
<Row className={'mt-2 items-center gap-2'}>
<div className={'btn-group justify-stretch'}>
{choices.map((choice, i) => {
return (
<button
key={choice.toString()}
disabled={isSubmitting}
className={clsx(
baseButtonClassName,
currentChoice === choice ? activeClasss : ''
)}
onClick={() => setChoice(choice)}
<RadioGroup
value={currentChoice.toString()}
onChange={(str) => null}
className="mt-2"
>
{titles[i]}
</button>
<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}
>
<RadioGroup.Label as="span">{choiceKey}</RadioGroup.Label>
</RadioGroup.Option>
))}
{children}
</div>
</RadioGroup>
</Row>
)
}

View File

@ -213,7 +213,7 @@ function EditableCloseDate(props: {
const [isEditingCloseTime, setIsEditingCloseTime] = useState(false)
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')

View File

@ -39,16 +39,21 @@ export function QuickBet(props: { contract: Contract }) {
const user = useUser()
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(
contract as FullContract<CPMM | DPM, Binary>,
userBets
contract as FullContract<DPM | CPMM, Binary | FreeResponseContract>,
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 =
contract.outcomeType === 'BINARY' ? yesFloorShares : noFloorShares
yesFloorShares || (noFloorShares && contract.outcomeType === 'NUMERIC')
const hasDownShares =
contract.outcomeType === 'BINARY' ? noFloorShares : yesFloorShares
noFloorShares && yesFloorShares <= 0 && contract.outcomeType !== 'NUMERIC'
const [upHover, setUpHover] = useState(false)
const [downHover, setDownHover] = useState(false)

View File

@ -21,14 +21,11 @@ export function CopyLinkDateTimeComponent(props: {
event: React.MouseEvent<HTMLAnchorElement, MouseEvent>
) {
event.preventDefault()
let elementLocation = `https://${ENV_CONFIG.domain}${contractPath(
contract
)}#${elementId}`
let currentLocation = window.location.href.includes('/home')
? `https://${ENV_CONFIG.domain}${contractPath(contract)}#${elementId}`
: window.location.href
if (currentLocation.includes('#')) {
currentLocation = currentLocation.split('#')[0]
}
copyToClipboard(`${currentLocation}#${elementId}`)
copyToClipboard(elementLocation)
setShowToast(true)
setTimeout(() => setShowToast(false), 2000)
}

View File

@ -156,7 +156,7 @@ export function FeedComment(props: {
<Row
className={clsx(
'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

View File

@ -127,13 +127,10 @@ export default function Sidebar(props: { className?: string }) {
const currentPage = router.pathname
const [countdown, setCountdown] = useState('...')
useEffect(() => {
const utcMidnightToLocalDate = new Date(getUtcFreeMarketResetTime(false))
const nextUtcResetTime = getUtcFreeMarketResetTime(false)
const interval = setInterval(() => {
const now = new Date().getTime()
let timeUntil = Math.abs(utcMidnightToLocalDate.getTime() - now)
if (now > utcMidnightToLocalDate.getTime()) {
timeUntil = 24 * 60 * 60 * 1000 - timeUntil
}
let timeUntil = nextUtcResetTime - now
const hoursUntil = timeUntil / 1000 / 60 / 60
const minutesUntil = Math.floor((hoursUntil * 60) % 60)
const secondsUntil = Math.floor((hoursUntil * 60 * 60) % 60)

View File

@ -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 { useEffect, useState } from 'react'
import { partition, sumBy } from 'lodash'
import { safeLocalStorage } from 'web/lib/util/local'
export const useSaveShares = (
contract: FullContract<CPMM | DPM, Binary>,
userBets: Bet[] | undefined
contract: FullContract<CPMM | DPM, Binary | FreeResponseContract>,
userBets: Bet[] | undefined,
freeResponseAnswerOutcome?: string
) => {
const [savedShares, setSavedShares] = useState<
| {
@ -17,9 +25,11 @@ export const useSaveShares = (
| undefined
>()
const [yesBets, noBets] = partition(
userBets ?? [],
(bet) => bet.outcome === 'YES'
// TODO: How do we handle numeric yes / no bets? - maybe bet amounts above vs below the highest peak
const [yesBets, noBets] = partition(userBets ?? [], (bet) =>
freeResponseAnswerOutcome
? bet.outcome === freeResponseAnswerOutcome
: bet.outcome === 'YES'
)
const [yesShares, noShares] = [
sumBy(yesBets, (bet) => bet.shares),
@ -30,18 +40,16 @@ export const useSaveShares = (
const noFloorShares = Math.round(noShares) === 0 ? 0 : Math.floor(noShares)
useEffect(() => {
const local = safeLocalStorage()
// 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) {
setSavedShares(JSON.parse(savedShares))
}
if (userBets) {
const updatedShares = { yesShares, noShares }
localStorage.setItem(
`${contract.id}-shares`,
JSON.stringify(updatedShares)
)
local?.setItem(`${contract.id}-shares`, JSON.stringify(updatedShares))
}
}, [contract.id, userBets, noShares, yesShares])

View File

@ -1,17 +1,36 @@
import { listContracts } from 'web/lib/firebase/contracts'
import { useEffect, useState } from 'react'
import { User } from 'common/user'
import dayjs from 'dayjs'
import utc from 'dayjs/plugin/utc'
dayjs.extend(utc)
let sessionCreatedContractToday = true
export function getUtcFreeMarketResetTime(yesterday: boolean) {
// Uses utc time like the server.
const utcFreeMarketResetTime = new Date()
utcFreeMarketResetTime.setUTCDate(
utcFreeMarketResetTime.getUTCDate() - (yesterday ? 1 : 0)
)
const utcFreeMarketMS = utcFreeMarketResetTime.setUTCHours(16, 0, 0, 0)
return utcFreeMarketMS
export function getUtcFreeMarketResetTime(previous: boolean) {
const localTimeNow = new Date()
const utc4pmToday = dayjs()
.utc()
.set('hour', 16)
.set('minute', 0)
.set('second', 0)
.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) => {

View File

@ -5,6 +5,7 @@ import { User } from 'common/user'
import { randomString } from 'common/util/random'
import './init'
import { functions } from './init'
import { safeLocalStorage } from '../util/local'
export const cloudFunction = <RequestData, ResponseData>(name: string) =>
httpsCallable<RequestData, ResponseData>(functions, name)
@ -48,10 +49,11 @@ export const resolveMarket = cloudFunction<
>('resolveMarket')
export const createUser: () => Promise<User | null> = () => {
let deviceToken = window.localStorage.getItem('device-token')
const local = safeLocalStorage()
let deviceToken = local?.getItem('device-token')
if (!deviceToken) {
deviceToken = randomString()
window.localStorage.setItem('device-token', deviceToken)
local?.setItem('device-token', deviceToken)
}
return cloudFunction('createUser')({ deviceToken })

View File

@ -27,6 +27,7 @@ import { getValue, getValues, listenForValue, listenForValues } from './utils'
import { DAY_MS } from 'common/util/time'
import { feed } from 'common/feed'
import { CATEGORY_LIST } from 'common/categories'
import { safeLocalStorage } from '../util/local'
export type { User }
@ -86,8 +87,9 @@ let createUserPromise: Promise<User | null> | undefined = undefined
const warmUpCreateUser = throttle(createUser, 5000 /* ms */)
export function listenForLogin(onUser: (user: User | null) => void) {
const cachedUser = localStorage.getItem(CACHED_USER_KEY)
onUser(cachedUser ? JSON.parse(cachedUser) : null)
const local = safeLocalStorage()
const cachedUser = local?.getItem(CACHED_USER_KEY)
onUser(cachedUser && JSON.parse(cachedUser))
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.
// Note: Cap on localStorage size is ~5mb
localStorage.setItem(CACHED_USER_KEY, JSON.stringify(user))
local?.setItem(CACHED_USER_KEY, JSON.stringify(user))
} else {
// User logged out; reset to null
onUser(null)
localStorage.removeItem(CACHED_USER_KEY)
local?.removeItem(CACHED_USER_KEY)
createUserPromise = undefined
}
})

11
web/lib/util/local.ts Normal file
View 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
}
}

View File

@ -18,7 +18,7 @@ import { getDailyNewUsers } from 'web/lib/firebase/users'
export const getStaticProps = fromPropz(getStaticPropz)
export async function getStaticPropz() {
const numberOfDays = 45
const numberOfDays = 90
const today = dayjs(dayjs().format('YYYY-MM-DD'))
// Convert from UTC midnight to PT midnight.
.add(7, 'hours')

View File

@ -17,8 +17,6 @@ import { useHasCreatedContractToday } from 'web/hooks/use-has-created-contract-t
import { removeUndefinedProps } from 'common/util/object'
import { CATEGORIES } from 'common/categories'
import { ChoicesToggleGroup } from 'web/components/choices-toggle-group'
import { CalculatorIcon } from '@heroicons/react/outline'
import { write } from 'fs'
export default function Create() {
const [question, setQuestion] = useState('')
@ -71,8 +69,6 @@ export function NewContract(props: { question: string; tag?: string }) {
const [minString, setMinString] = useState('')
const [maxString, setMaxString] = useState('')
const [description, setDescription] = useState('')
const [showCalendar, setShowCalendar] = useState(false)
const [showNumInput, setShowNumInput] = useState(false)
const [category, setCategory] = useState<string>('')
// 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
const [closeDate, setCloseDate] = useState<undefined | string>(weekFrom(dayjs()))
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 closeTime = closeDate ? dayjs(closeDate).valueOf() : undefined
const closeTime = closeDate
? dayjs(`${closeDate}T${closeHoursMinutes}`).valueOf()
: undefined
const automaticResolutionTime = resolutionDate ? dayjs(resolutionDate).valueOf() : undefined
const balance = creator?.balance || 0
const min = minString ? parseFloat(minString) : 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 =
initialProb > 0 &&
initialProb < 100 &&
(outcomeType === 'BINARY' ? initialProb >= 5 && initialProb <= 95 : true) &&
question.length > 0 &&
ante !== undefined &&
ante !== null &&
@ -130,8 +131,7 @@ export function NewContract(props: { question: string; tag?: string }) {
}
function setCloseDateInDays(days: number) {
setShowCalendar(days === 0)
const newCloseDate = dayjs().add(days, 'day').format('YYYY-MM-DDT23:59')
const newCloseDate = dayjs().add(days, 'day').format('YYYY-MM-DD')
setCloseDate(newCloseDate)
}
@ -173,90 +173,83 @@ export function NewContract(props: { question: string; tag?: string }) {
return (
<div>
<label className="label mt-1">
<span className="my-1">Answer type</span>
<span className="mt-1">Answer type</span>
</label>
<Row className="form-control gap-2">
<label className="label cursor-pointer gap-2">
<input
className="radio"
type="radio"
name="opt"
checked={outcomeType === 'BINARY'}
value="BINARY"
onChange={() => setOutcomeType('BINARY')}
disabled={isSubmitting}
<ChoicesToggleGroup
currentChoice={outcomeType}
setChoice={(choice) => {
if (choice === 'NUMERIC')
setMarketInfoText(
'Numeric markets are still experimental and subject to major revisions.'
)
else if (choice === 'FREE_RESPONSE')
setMarketInfoText(
'Users can submit their own answers to this market.'
)
else setMarketInfoText('')
setOutcomeType(choice as outcomeType)
}}
choicesMap={{
'Yes / No': 'BINARY',
'Free response': 'FREE_RESPONSE',
Numeric: 'NUMERIC',
}}
isSubmitting={isSubmitting}
className={'col-span-4'}
/>
<span className="label-text">Yes / No</span>
</label>
<label className="label cursor-pointer gap-2">
<input
className="radio"
type="radio"
name="opt"
checked={outcomeType === 'FREE_RESPONSE'}
value="FREE_RESPONSE"
onChange={() => setOutcomeType('FREE_RESPONSE')}
disabled={isSubmitting}
/>
<span className="label-text">Free response</span>
</label>
<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>
{marketInfoText && (
<div className="mt-2 ml-1 text-sm text-indigo-700">
{marketInfoText}
</div>
)}
<Spacer h={4} />
{outcomeType === 'BINARY' && (
<div className="form-control">
<Row className="label justify-start">
<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 className={'w-full items-center sm:gap-2'}>
<Row className={'justify-start'}>
<ChoicesToggleGroup
currentChoice={initialProb}
setChoice={setInitialProb}
choices={[25, 50, 75]}
titles={['Unlikely', 'Unsure', 'Likely']}
setChoice={(option) => {
setProbErrorText('')
setInitialProb(option as number)
}}
choicesMap={{
Unlikely: 25,
'Not Sure': 50,
Likely: 75,
}}
isSubmitting={isSubmitting}
/>
{showNumInput && (
<>
className={'col-span-4 sm:col-span-3'}
>
<Row className={'col-span-3 items-center justify-start'}>
<input
type="number"
value={initialProb}
className={
'max-w-[16%] sm:max-w-[15%] ' +
'input-bordered input-md mt-2 rounded-md p-1 text-lg sm:p-4'
'input-bordered input-md max-w-[100px] rounded-md border-gray-300 pr-2 text-lg'
}
min={5}
max={95}
disabled={isSubmitting}
min={10}
max={90}
onChange={(e) =>
setInitialProb(parseInt(e.target.value.substring(0, 2)))
}
onChange={(e) => {
// show error if prob is less than 5 or greater than 95:
const prob = parseInt(e.target.value)
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>
{probErrorText && (
<div className="text-error mt-2 ml-1 text-sm">{probErrorText}</div>
)}
</div>
)}
@ -299,7 +292,7 @@ export function NewContract(props: { question: string; tag?: string }) {
<div className="form-control mb-1 items-start">
<label className="label mb-1 gap-2">
<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>
<Textarea
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">
<label className="label mb-1 gap-2">
<span>Question expires in a:</span>
<InfoTooltip text="Betting will be halted after this date (local timezone)." />
<span>Question closes in:</span>
<InfoTooltip text="Betting will be halted after this time (local timezone)." />
</label>
<Row className={'w-full items-center gap-2'}>
<ChoicesToggleGroup
currentChoice={
closeDate
? [1, 7, 30, 365, 0].includes(
dayjs(closeDate).diff(dayjs(), 'day')
)
? dayjs(closeDate).diff(dayjs(), 'day')
: 0
: -1
}
setChoice={setCloseDateInDays}
choices={[1, 7, 30, 0]}
titles={['Day', 'Week', 'Month', 'Custom']}
currentChoice={dayjs(`${closeDate}T23:59`).diff(dayjs(), 'day')}
setChoice={(choice) => {
setCloseDateInDays(choice as number)
}}
choicesMap={{
'A day': 1,
'A week': 7,
'30 days': 30,
'This year': daysLeftInTheYear,
}}
isSubmitting={isSubmitting}
className={'col-span-4 sm:col-span-2'}
/>
</Row>
{showCalendar && (
<Row>
<input
type={'date'}
className="input input-bordered mt-4"
onClick={(e) => e.stopPropagation()}
onChange={(e) =>
setCloseDate(
dayjs(e.target.value).format('YYYY-MM-DDT23:59') || ''
)
setCloseDate(dayjs(e.target.value).format('YYYY-MM-DD') || '')
}
min={Date.now()}
disabled={isSubmitting}
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>
{outcomeType === 'BINARY' && (
@ -413,18 +412,23 @@ export function NewContract(props: { question: string; tag?: string }) {
</div>
{resolutionType === 'COMBINED' && (
<div className="form-control mb-1 items-start">
<label className="label mb-1 gap-2">
<div className="form-control">
<Row className="label justify-start">
<span>Question resolves automatically as:</span>
<InfoTooltip text="The market will be resolved automatically on this date (local timezone)." />
</label>
<Row className={'w-full items-center gap-2'}>
</Row>
<Row className={'justify-start'}>
<ChoicesToggleGroup
currentChoice={automaticResolution}
setChoice={setAutomaticResolution}
choices={RESOLUTIONS}
titles={['YES', 'NO', 'PROB', 'N/A']}
setChoice={(choice) => setAutomaticResolution(choice as resolution)}
choicesMap={{
'YES': 'YES',
'NO': 'NO',
'MKT': 'PROB',
'CANCEL': 'N/A',
}}
isSubmitting={isSubmitting}
className={'col-span-4 sm:col-span-3'}
/>
</Row>
<input
@ -453,7 +457,7 @@ export function NewContract(props: { question: string; tag?: string }) {
{mustWaitForDailyFreeMarketStatus != 'loading' &&
mustWaitForDailyFreeMarketStatus && (
<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>
@ -512,7 +516,7 @@ export function NewContract(props: { question: string; tag?: string }) {
submit()
}}
>
{isSubmitting ? 'Creating...' : 'Create market'}
{isSubmitting ? 'Creating...' : 'Create question'}
</button>
</div>
</div>

View File

@ -54,7 +54,7 @@ export async function getStaticPropz(props: { params: { slugs: string[] } }) {
)
activeContracts = [...unresolved, ...resolved]
const creatorScores = scoreCreators(contracts, bets)
const creatorScores = scoreCreators(contracts)
const traderScores = scoreTraders(contracts, bets)
const [topCreators, topTraders] = await Promise.all([
toTopUsers(creatorScores),