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'],
|
||||
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'],
|
||||
},
|
||||
|
|
|
@ -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] })
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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.
|
||||
}
|
||||
|
|
|
@ -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
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'
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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]]
|
||||
|
|
|
@ -11,6 +11,7 @@ module.exports = {
|
|||
plugins: ['@typescript-eslint'],
|
||||
parser: '@typescript-eslint/parser',
|
||||
parserOptions: {
|
||||
tsconfigRootDir: __dirname,
|
||||
project: ['./tsconfig.json'],
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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`
|
||||
|
||||
|
|
|
@ -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()}
|
||||
<RadioGroup
|
||||
value={currentChoice.toString()}
|
||||
onChange={(str) => null}
|
||||
className="mt-2"
|
||||
>
|
||||
<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}
|
||||
className={clsx(
|
||||
baseButtonClassName,
|
||||
currentChoice === choice ? activeClasss : ''
|
||||
)}
|
||||
onClick={() => setChoice(choice)}
|
||||
>
|
||||
{titles[i]}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<RadioGroup.Label as="span">{choiceKey}</RadioGroup.Label>
|
||||
</RadioGroup.Option>
|
||||
))}
|
||||
{children}
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</Row>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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])
|
||||
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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 })
|
||||
|
|
|
@ -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
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 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')
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
<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>
|
||||
<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'}
|
||||
/>
|
||||
{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">
|
||||
<span>Question resolves automatically as:</span>
|
||||
<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>
|
||||
|
|
|
@ -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),
|
||||
|
|
Loading…
Reference in New Issue
Block a user