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