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'], 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'],
}, },

View File

@ -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] })

View File

@ -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) {

View File

@ -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.
}

View File

@ -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
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' 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)
} }

View File

@ -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]]

View File

@ -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'],
}, },
}, },

View File

@ -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`

View File

@ -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()}
disabled={isSubmitting}
className={clsx(
baseButtonClassName,
currentChoice === choice ? activeClasss : ''
)}
onClick={() => setChoice(choice)}
> >
{titles[i]} <div className={`grid grid-cols-12 gap-3`}>
</button> {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> </div>
</RadioGroup>
</Row> </Row>
) )
} }

View File

@ -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')

View File

@ -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)

View File

@ -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)
} }

View File

@ -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

View File

@ -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)

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 { 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])

View File

@ -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) => {

View File

@ -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 })

View File

@ -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
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 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')

View File

@ -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.'
)
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> {marketInfoText && (
</label> <div className="mt-2 ml-1 text-sm text-indigo-700">
{marketInfoText}
<label className="label cursor-pointer gap-2"> </div>
<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>
<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> </Row>
</ChoicesToggleGroup>
</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>

View File

@ -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),