Merge branch 'main' into atlas

This commit is contained in:
Austin Chen 2022-06-14 00:46:25 -07:00
commit a1368abd66
44 changed files with 379 additions and 586 deletions

View File

@ -0,0 +1,27 @@
import { groupBy, mapValues, sum, sumBy } from 'lodash'
import { Txn } from './txn'
// Returns a map of charity ids to the amount of M$ matched
export function quadraticMatches(
allCharityTxns: Txn[],
matchingPool: number
): Record<string, number> {
// For each charity, group the donations by each individual donor
const donationsByCharity = groupBy(allCharityTxns, 'toId')
const donationsByDonors = mapValues(donationsByCharity, (txns) =>
groupBy(txns, 'fromId')
)
// Weight for each charity = [sum of sqrt(individual donor)] ^ 2
const weights = mapValues(donationsByDonors, (byDonor) => {
const sumByDonor = Object.values(byDonor).map((txns) =>
sumBy(txns, 'amount')
)
const sumOfRoots = sumBy(sumByDonor, Math.sqrt)
return sumOfRoots ** 2
})
// Then distribute the matching pool based on the individual weights
const totalWeight = sum(Object.values(weights))
return mapValues(weights, (weight) => matchingPool * (weight / totalWeight))
}

View File

@ -1,3 +1,5 @@
import { sortBy } from 'lodash'
export const logInterpolation = (min: number, max: number, value: number) => {
if (value <= min) return 0
if (value >= max) return 1
@ -16,4 +18,15 @@ export function normpdf(x: number, mean = 0, variance = 1) {
)
}
const TAU = Math.PI * 2
export const TAU = Math.PI * 2
export function median(values: number[]) {
if (values.length === 0) return NaN
const sorted = sortBy(values, (x) => x)
const mid = Math.floor(sorted.length / 2)
if (sorted.length % 2 === 0) {
return (sorted[mid - 1] + sorted[mid]) / 2
}
return sorted[mid]
}

View File

@ -3,7 +3,7 @@ import * as admin from 'firebase-admin'
import { z } from 'zod'
import { APIError, newEndpoint, validate } from './api'
import { Contract } from '../../common/contract'
import { Contract, CPMM_MIN_POOL_QTY } from '../../common/contract'
import { User } from '../../common/user'
import { getCpmmSellBetInfo } from '../../common/sell-bet'
import { addObjects, removeUndefinedProps } from '../../common/util/object'
@ -57,8 +57,12 @@ export const sellshares = newEndpoint(['POST'], async (req, auth) => {
prevLoanAmount
)
if (!isFinite(newP)) {
throw new APIError(500, 'Trade rejected due to overflow error.')
if (
!newP ||
!isFinite(newP) ||
Math.min(...Object.values(newPool ?? {})) < CPMM_MIN_POOL_QTY
) {
throw new APIError(400, 'Sale too large for current liquidity pool.')
}
const newBetDoc = firestore.collection(`contracts/${contractId}/bets`).doc()

View File

@ -8,8 +8,15 @@ module.exports = {
],
rules: {
'@typescript-eslint/no-empty-function': 'off',
'@typescript-eslint/no-unused-vars': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-unused-vars': [
'error',
{
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
caughtErrorsIgnorePattern: '^_',
},
],
'@next/next/no-img-element': 'off',
'@next/next/no-typos': 'off',
'lodash/import-scope': [2, 'member'],

View File

@ -24,7 +24,7 @@ export function AddFundsButton(props: { className?: string }) {
className
)}
>
Add funds
Get M$
</label>
<input type="checkbox" id="add-funds" className="modal-toggle" />

View File

@ -0,0 +1,24 @@
import { ExclamationIcon } from '@heroicons/react/solid'
import { Linkify } from './linkify'
export function AlertBox(props: { title: string; text: string }) {
const { title, text } = props
return (
<div className="rounded-md bg-yellow-50 p-4">
<div className="flex">
<div className="flex-shrink-0">
<ExclamationIcon
className="h-5 w-5 text-yellow-400"
aria-hidden="true"
/>
</div>
<div className="ml-3">
<h3 className="text-sm font-medium text-yellow-800">{title}</h3>
<div className="mt-2 text-sm text-yellow-700">
<Linkify text={text} />
</div>
</div>
</div>
</div>
)
}

View File

@ -1,5 +1,5 @@
import { sortBy, partition, sum, uniq } from 'lodash'
import { useLayoutEffect, useState } from 'react'
import { useEffect, useState } from 'react'
import { FreeResponseContract } from 'common/contract'
import { Col } from '../layout/col'
@ -85,7 +85,7 @@ export function AnswersPanel(props: { contract: FreeResponseContract }) {
})
}
useLayoutEffect(() => {
useEffect(() => {
setChosenAnswers({})
}, [resolveOption])
@ -116,7 +116,7 @@ export function AnswersPanel(props: { contract: FreeResponseContract }) {
{!resolveOption && (
<div className={clsx('flow-root pr-2 md:pr-0')}>
<div className={clsx(tradingAllowed(contract) ? '' : '-mb-6')}>
{answerItems.map((item, activityItemIdx) => (
{answerItems.map((item) => (
<div key={item.id} className={'relative pb-2'}>
<div className="relative flex items-start space-x-3">
<OpenAnswer {...item} />

View File

@ -138,9 +138,8 @@ export function BetsList(props: { user: User; hideBetsBefore?: number }) {
return !hasSoldAll
})
const [settled, unsettled] = partition(
contracts,
(c) => c.isResolved || contractsMetrics[c.id].invested === 0
const unsettled = contracts.filter(
(c) => !c.isResolved && contractsMetrics[c.id].invested !== 0
)
const currentInvested = sumBy(
@ -261,7 +260,7 @@ function ContractBets(props: {
const isBinary = outcomeType === 'BINARY'
const { payout, profit, profitPercent, invested } = getContractBetMetrics(
const { payout, profit, profitPercent } = getContractBetMetrics(
contract,
bets
)
@ -657,7 +656,6 @@ function SellButton(props: { contract: Contract; bet: Bet }) {
return (
<ConfirmationButton
id={`sell-${bet.id}`}
openModalBtn={{
className: clsx('btn-sm', isSubmitting && 'btn-disabled loading'),
label: 'Sell',

View File

@ -6,9 +6,11 @@ import { Charity } from 'common/charity'
import { useCharityTxns } from 'web/hooks/use-charity-txns'
import { manaToUSD } from '../../../common/util/format'
import { Row } from '../layout/row'
import { Col } from '../layout/col'
export function CharityCard(props: { charity: Charity }) {
const { name, slug, photo, preview, id, tags } = props.charity
export function CharityCard(props: { charity: Charity; match?: number }) {
const { charity, match } = props
const { slug, photo, preview, id, tags } = charity
const txns = useCharityTxns(id)
const raised = sumBy(txns, (txn) => txn.amount)
@ -32,14 +34,22 @@ export function CharityCard(props: { charity: Charity }) {
{/* <h3 className="card-title line-clamp-3">{name}</h3> */}
<div className="line-clamp-4 text-sm">{preview}</div>
{raised > 0 && (
<Row className="text-primary mt-4 flex-1 items-end justify-center gap-2">
<span className="text-3xl">
{raised < 100
? manaToUSD(raised)
: '$' + Math.floor(raised / 100)}
</span>
<span>raised</span>
</Row>
<>
<Row className="mt-4 flex-1 items-end justify-center gap-6 text-gray-900">
<Col>
<span className="text-3xl font-semibold">
{formatUsd(raised)}
</span>
<span>raised</span>
</Col>
{match && (
<Col className="text-gray-500">
<span className="text-xl">+{formatUsd(match)}</span>
<span className="">match</span>
</Col>
)}
</Row>
</>
)}
</div>
</div>
@ -47,6 +57,10 @@ export function CharityCard(props: { charity: Charity }) {
)
}
function formatUsd(mana: number) {
return mana < 100 ? manaToUSD(mana) : '$' + Math.floor(mana / 100)
}
function FeaturedBadge() {
return (
<span className="inline-flex items-center gap-1 bg-yellow-100 px-3 py-0.5 text-sm font-medium text-yellow-800">

View File

@ -24,13 +24,12 @@ export function ChoicesToggleGroup(props: {
<RadioGroup
className={clsx(className, 'flex flex-row flex-wrap items-center gap-3')}
value={currentChoice.toString()}
onChange={(str) => null}
onChange={setChoice}
>
{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' : '',

View File

@ -5,7 +5,6 @@ import { Modal } from './layout/modal'
import { Row } from './layout/row'
export function ConfirmationButton(props: {
id: string
openModalBtn: {
label: string
icon?: JSX.Element
@ -22,7 +21,7 @@ export function ConfirmationButton(props: {
onSubmit: () => void
children: ReactNode
}) {
const { id, openModalBtn, cancelBtn, submitBtn, onSubmit, children } = props
const { openModalBtn, cancelBtn, submitBtn, onSubmit, children } = props
const [open, setOpen] = useState(false)
@ -67,7 +66,6 @@ export function ResolveConfirmationButton(props: {
props
return (
<ConfirmationButton
id="resolution-modal"
openModalBtn={{
className: clsx(
'border-none self-start',

View File

@ -140,7 +140,7 @@ export function QuickBet(props: { contract: Contract; user: User }) {
}
}
const textColor = `text-${getColor(contract, previewProb)}`
const textColor = `text-${getColor(contract)}`
return (
<Col
@ -223,7 +223,7 @@ export function QuickBet(props: { contract: Contract; user: User }) {
export function ProbBar(props: { contract: Contract; previewProb?: number }) {
const { contract, previewProb } = props
const color = getColor(contract, previewProb)
const color = getColor(contract)
const prob = previewProb ?? getProb(contract)
return (
<>
@ -257,7 +257,7 @@ function QuickOutcomeView(props: {
// If there's a preview prob, display that instead of the current prob
const override =
previewProb === undefined ? undefined : formatPercent(previewProb)
const textColor = `text-${getColor(contract, previewProb)}`
const textColor = `text-${getColor(contract)}`
let display: string | undefined
switch (outcomeType) {
@ -306,7 +306,7 @@ function getNumericScale(contract: NumericContract) {
return (ev - min) / (max - min)
}
export function getColor(contract: Contract, previewProb?: number) {
export function getColor(contract: Contract) {
// TODO: Try injecting a gradient here
// return 'primary'
const { resolution } = contract

View File

@ -1,156 +0,0 @@
import { sample } from 'lodash'
import { SparklesIcon, XIcon } from '@heroicons/react/solid'
import { Avatar } from './avatar'
import { useEffect, useRef, useState } from 'react'
import { Spacer } from './layout/spacer'
import { NewContract } from '../pages/create'
import { firebaseLogin, User } from 'web/lib/firebase/users'
import { ContractsGrid } from './contract/contracts-list'
import { Contract, MAX_QUESTION_LENGTH } from 'common/contract'
import { Col } from './layout/col'
import clsx from 'clsx'
import { Row } from './layout/row'
import { ENV_CONFIG } from '../../common/envs/constants'
import { SiteLink } from './site-link'
import { formatMoney } from 'common/util/format'
export function FeedPromo(props: { hotContracts: Contract[] }) {
const { hotContracts } = props
return (
<>
<Col className="mb-6 rounded-xl text-center sm:m-12 sm:mt-0">
<img
height={250}
width={250}
className="self-center"
src="/flappy-logo.gif"
/>
<h1 className="text-3xl sm:text-6xl xl:text-6xl">
<div className="font-semibold sm:mb-2">
Bet on{' '}
<span className="bg-gradient-to-r from-teal-400 to-green-400 bg-clip-text font-bold text-transparent">
anything!
</span>
</div>
</h1>
<Spacer h={6} />
<div className="mb-4 px-2 text-gray-500">
Bet on any topic imaginable with play-money markets. Or create your
own!
<br />
<br />
Sign up and get {formatMoney(1000)} - worth $10 to your{' '}
<SiteLink className="font-semibold" href="/charity">
favorite charity.
</SiteLink>
<br />
</div>
<Spacer h={6} />
<button
className="self-center rounded-md border-none bg-gradient-to-r from-teal-500 to-green-500 py-4 px-6 text-lg font-semibold normal-case text-white hover:from-teal-600 hover:to-green-600"
onClick={firebaseLogin}
>
Start betting now
</button>{' '}
</Col>
<Row className="m-4 mb-6 items-center gap-1 text-xl font-semibold text-gray-800">
<SparklesIcon className="inline h-5 w-5" aria-hidden="true" />
Trending markets
</Row>
<ContractsGrid
contracts={hotContracts?.slice(0, 10) || []}
loadMore={() => {}}
hasMore={false}
/>
</>
)
}
export default function FeedCreate(props: {
user?: User
tag?: string
placeholder?: string
className?: string
}) {
const { user, tag, className } = props
const [question, setQuestion] = useState('')
const [isExpanded, setIsExpanded] = useState(false)
const inputRef = useRef<HTMLTextAreaElement | null>()
// Rotate through a new placeholder each day
// Easter egg idea: click your own name to shuffle the placeholder
// const daysSinceEpoch = Math.floor(Date.now() / 1000 / 60 / 60 / 24)
// Take care not to produce a different placeholder on the server and client
const [defaultPlaceholder, setDefaultPlaceholder] = useState('')
useEffect(() => {
setDefaultPlaceholder(`e.g. ${sample(ENV_CONFIG.newQuestionPlaceholders)}`)
}, [])
const placeholder = props.placeholder ?? defaultPlaceholder
return (
<div
className={clsx(
'w-full cursor-text rounded bg-white p-4 shadow-md',
isExpanded ? 'ring-2 ring-indigo-300' : '',
className
)}
onClick={() => {
!isExpanded && inputRef.current?.focus()
}}
>
<div className="relative flex items-start space-x-3">
<Avatar username={user?.username} avatarUrl={user?.avatarUrl} noLink />
<div className="min-w-0 flex-1">
<Row className="justify-between">
<p className="my-0.5 text-sm">Ask a question... </p>
{isExpanded && (
<button
className="btn btn-xs btn-circle btn-ghost rounded"
onClick={() => setIsExpanded(false)}
>
<XIcon
className="mx-auto h-6 w-6 text-gray-500"
aria-hidden="true"
/>
</button>
)}
</Row>
<textarea
ref={inputRef as any}
className={clsx(
'w-full resize-none appearance-none border-transparent bg-transparent p-0 text-indigo-700 placeholder:text-gray-400 focus:border-transparent focus:ring-transparent',
question && 'text-lg sm:text-xl',
!question && 'text-base sm:text-lg'
)}
placeholder={placeholder}
value={question}
rows={question.length > 68 ? 4 : 2}
maxLength={MAX_QUESTION_LENGTH}
onClick={(e) => e.stopPropagation()}
onChange={(e) => setQuestion(e.target.value.replace('\n', ''))}
onFocus={() => setIsExpanded(true)}
/>
</div>
</div>
{/* Hide component instead of deleting, so edits to NewContract don't get lost */}
<div className={isExpanded ? '' : 'hidden'}>
<NewContract question={question} tag={tag} />
</div>
{/* Show a fake "Create Market" button, which gets replaced with the NewContract one*/}
{!isExpanded && (
<div className="flex justify-end sm:-mt-4">
<button className="btn btn-sm capitalize" disabled>
Create Market
</button>
</div>
)}
</div>
)
}

View File

@ -2,7 +2,7 @@ import { Answer } from 'common/answer'
import { Bet } from 'common/bet'
import { Comment } from 'common/comment'
import { formatPercent } from 'common/util/format'
import React, { useEffect, useMemo, useState } from 'react'
import React, { useEffect, useState } from 'react'
import { Col } from 'web/components/layout/col'
import { Modal } from 'web/components/layout/modal'
import { AnswerBetPanel } from 'web/components/answers/answer-bet-panel'

View File

@ -3,7 +3,6 @@ import React, { useState } from 'react'
import {
BanIcon,
CheckIcon,
DotsVerticalIcon,
LockClosedIcon,
XIcon,
} from '@heroicons/react/solid'
@ -274,30 +273,3 @@ function FeedClose(props: { contract: Contract }) {
</>
)
}
// TODO: Should highlight the entire Feed segment
function FeedExpand(props: { setExpanded: (expanded: boolean) => void }) {
const { setExpanded } = props
return (
<>
<button onClick={() => setExpanded(true)}>
<div className="relative px-1">
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-gray-200 hover:bg-gray-300">
<DotsVerticalIcon
className="h-5 w-5 text-gray-500"
aria-hidden="true"
/>
</div>
</div>
</button>
<button onClick={() => setExpanded(true)}>
<div className="min-w-0 flex-1 py-1.5">
<div className="text-sm text-gray-500 hover:text-gray-700">
<span>Show all activity</span>
</div>
</div>
</button>
</>
)
}

View File

@ -48,7 +48,6 @@ export function CreateFoldButton() {
return (
<ConfirmationButton
id="create-fold"
openModalBtn={{
label: 'New',
icon: <PlusCircleIcon className="mr-2 h-5 w-5" />,

View File

@ -0,0 +1,65 @@
import { SparklesIcon } from '@heroicons/react/solid'
import { Contract } from 'common/contract'
import { Spacer } from './layout/spacer'
import { firebaseLogin } from 'web/lib/firebase/users'
import { ContractsGrid } from './contract/contracts-list'
import { Col } from './layout/col'
import { Row } from './layout/row'
export function LandingPagePanel(props: { hotContracts: Contract[] }) {
const { hotContracts } = props
return (
<>
<Col className="mb-6 rounded-xl sm:m-12 sm:mt-0">
<img
height={250}
width={250}
className="self-center"
src="/flappy-logo.gif"
/>
<div className="m-4 max-w-[550px] self-center">
<h1 className="text-3xl sm:text-6xl xl:text-6xl">
<div className="font-semibold sm:mb-2">
Predict{' '}
<span className="bg-gradient-to-r from-indigo-500 to-blue-500 bg-clip-text font-bold text-transparent">
anything!
</span>
</div>
</h1>
<Spacer h={6} />
<div className="mb-4 px-2 ">
Create a play-money prediction market on any topic you care about
and bet with your friends on what will happen!
<br />
{/* <br />
Sign up and get {formatMoney(1000)} - worth $10 to your{' '}
<SiteLink className="font-semibold" href="/charity">
favorite charity.
</SiteLink>
<br /> */}
</div>
</div>
<Spacer h={6} />
<button
className="self-center rounded-md border-none bg-gradient-to-r from-indigo-500 to-blue-500 py-4 px-6 text-lg font-semibold normal-case text-white hover:from-indigo-600 hover:to-blue-600"
onClick={firebaseLogin}
>
Get started
</button>{' '}
</Col>
<Row className="m-4 mb-6 items-center gap-1 text-xl font-semibold text-gray-800">
<SparklesIcon className="inline h-5 w-5" aria-hidden="true" />
Trending markets
</Row>
<ContractsGrid
contracts={hotContracts?.slice(0, 10) || []}
loadMore={() => {}}
hasMore={false}
/>
</>
)
}

View File

@ -98,7 +98,7 @@ function AddLiquidityPanel(props: { contract: CPMMContract }) {
setError('Server error')
}
})
.catch((e) => setError('Server error'))
.catch((_) => setError('Server error'))
}
return (
@ -136,7 +136,7 @@ function AddLiquidityPanel(props: { contract: CPMMContract }) {
function ViewLiquidityPanel(props: { contract: CPMMContract }) {
const { contract } = props
const { id: contractId, pool } = contract
const { pool } = contract
const { YES: yesShares, NO: noShares } = pool
return (
@ -162,7 +162,7 @@ function WithdrawLiquidityPanel(props: {
const { contract, lpShares } = props
const { YES: yesShares, NO: noShares } = lpShares
const [error, setError] = useState<string | undefined>(undefined)
const [_error, setError] = useState<string | undefined>(undefined)
const [isSuccess, setIsSuccess] = useState(false)
const [isLoading, setIsLoading] = useState(false)
@ -171,12 +171,12 @@ function WithdrawLiquidityPanel(props: {
setIsSuccess(false)
withdrawLiquidity({ contractId: contract.id })
.then((r) => {
.then((_) => {
setIsSuccess(true)
setError(undefined)
setIsLoading(false)
})
.catch((e) => setError('Server error'))
.catch((_) => setError('Server error'))
}
if (isSuccess)

View File

@ -1,8 +1,7 @@
import Link from 'next/link'
import { firebaseLogout, User } from 'web/lib/firebase/users'
import { User } from 'web/lib/firebase/users'
import { formatMoney } from 'common/util/format'
import { Avatar } from '../avatar'
import { IS_PRIVATE_MANIFOLD } from 'common/envs/constants'
export function ProfileSummary(props: { user: User }) {
const { user } = props

View File

@ -7,15 +7,12 @@ import {
CashIcon,
HeartIcon,
PresentationChartLineIcon,
ChatAltIcon,
SparklesIcon,
NewspaperIcon,
} from '@heroicons/react/outline'
import clsx from 'clsx'
import { sortBy } from 'lodash'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { useFollowedFolds } from 'web/hooks/use-fold'
import { useUser } from 'web/hooks/use-user'
import { firebaseLogin, firebaseLogout, User } from 'web/lib/firebase/users'
import { ManifoldLogo } from './manifold-logo'
@ -51,7 +48,7 @@ function getNavigation(username: string) {
icon: NotificationsIcon,
},
{ name: 'Charity', href: '/charity', icon: HeartIcon },
{ name: 'Get M$', href: '/add-funds', icon: CashIcon },
]
}
@ -63,14 +60,15 @@ function getMoreNavigation(user?: User | null) {
if (!user) {
return [
{ name: 'Leaderboards', href: '/leaderboards' },
{ name: 'Charity', href: '/charity' },
{ name: 'Discord', href: 'https://discord.gg/eHQBNBqXuh' },
{ name: 'Twitter', href: 'https://twitter.com/ManifoldMarkets' },
]
}
return [
{ name: 'Add funds', href: '/add-funds' },
{ name: 'Leaderboards', href: '/leaderboards' },
{ name: 'Charity', href: '/charity' },
{ name: 'Blog', href: 'https://news.manifold.markets' },
{ name: 'Discord', href: 'https://discord.gg/eHQBNBqXuh' },
{ name: 'Twitter', href: 'https://twitter.com/ManifoldMarkets' },
@ -104,7 +102,7 @@ const signedOutMobileNavigation = [
]
const mobileNavigation = [
{ name: 'Add funds', href: '/add-funds', icon: CashIcon },
{ name: 'Get M$', href: '/add-funds', icon: CashIcon },
...signedOutMobileNavigation,
]
@ -179,8 +177,6 @@ export default function Sidebar(props: { className?: string }) {
}, [])
const user = useUser()
let folds = useFollowedFolds(user) || []
folds = sortBy(folds, 'followCount').reverse()
const mustWaitForFreeMarketStatus = useHasCreatedContractToday(user)
const navigationOptions =
user === null

View File

@ -53,7 +53,6 @@ function NumericBuyPanel(props: {
const [betAmount, setBetAmount] = useState<number | undefined>(undefined)
const [valueError, setValueError] = useState<string | undefined>()
const [error, setError] = useState<string | undefined>()
const [isSubmitting, setIsSubmitting] = useState(false)
const [wasSubmitted, setWasSubmitted] = useState(false)

View File

@ -6,12 +6,11 @@ import { Toaster } from 'react-hot-toast'
export function Page(props: {
margin?: boolean
assertUser?: 'signed-in' | 'signed-out'
rightSidebar?: ReactNode
suspend?: boolean
children?: ReactNode
}) {
const { margin, assertUser, children, rightSidebar, suspend } = props
const { margin, children, rightSidebar, suspend } = props
return (
<>

View File

@ -1,4 +1,8 @@
import clsx from 'clsx'
import { uniq } from 'lodash'
import { LinkIcon } from '@heroicons/react/solid'
import { PencilIcon } from '@heroicons/react/outline'
import { follow, unfollow, User } from 'web/lib/firebase/users'
import { CreatorContractsList } from './contract/contracts-list'
import { SEO } from './SEO'
@ -9,9 +13,7 @@ import { Col } from './layout/col'
import { Linkify } from './linkify'
import { Spacer } from './layout/spacer'
import { Row } from './layout/row'
import { LinkIcon } from '@heroicons/react/solid'
import { genHash } from 'common/util/random'
import { PencilIcon } from '@heroicons/react/outline'
import { Tabs } from './layout/tabs'
import { UserCommentsList } from './comments-list'
import { useEffect, useState } from 'react'
@ -22,8 +24,10 @@ import { LoadingIndicator } from './loading-indicator'
import { BetsList } from './bets-list'
import { Bet } from 'common/bet'
import { getUserBets } from 'web/lib/firebase/bets'
import { uniq } from 'lodash'
import { FollowersButton, FollowingButton } from './following-button'
import { AlertBox } from './alert-box'
import { useFollows } from 'web/hooks/use-follows'
import { FollowButton } from './follow-button'
export function UserLink(props: {
name: string
@ -305,29 +309,3 @@ export function defaultBannerUrl(userId: string) {
]
return defaultBanner[genHash(userId)() % defaultBanner.length]
}
import { ExclamationIcon } from '@heroicons/react/solid'
import { FollowButton } from './follow-button'
import { useFollows } from 'web/hooks/use-follows'
function AlertBox(props: { title: string; text: string }) {
const { title, text } = props
return (
<div className="rounded-md bg-yellow-50 p-4">
<div className="flex">
<div className="flex-shrink-0">
<ExclamationIcon
className="h-5 w-5 text-yellow-400"
aria-hidden="true"
/>
</div>
<div className="ml-3">
<h3 className="text-sm font-medium text-yellow-800">{title}</h3>
<div className="mt-2 text-sm text-yellow-700">
<Linkify text={text} />
</div>
</div>
</div>
</div>
)
}

View File

@ -168,7 +168,7 @@ export function FundsSelector(props: {
{fundAmounts.map((amount) => (
<Button
key={amount}
color={selected === amount ? 'green' : 'gray'}
color={selected === amount ? 'indigo' : 'gray'}
onClick={() => onSelect(amount as any)}
className={btnClassName}
>
@ -230,7 +230,7 @@ export function NumberCancelSelector(props: {
function Button(props: {
className?: string
onClick?: () => void
color: 'green' | 'red' | 'blue' | 'yellow' | 'gray'
color: 'green' | 'red' | 'blue' | 'indigo' | 'yellow' | 'gray'
children?: ReactNode
}) {
const { className, onClick, children, color } = props
@ -244,6 +244,7 @@ function Button(props: {
color === 'red' && 'bg-red-400 text-white hover:bg-red-500',
color === 'yellow' && 'bg-yellow-400 text-white hover:bg-yellow-500',
color === 'blue' && 'bg-blue-400 text-white hover:bg-blue-500',
color === 'indigo' && 'bg-indigo-500 text-white hover:bg-indigo-600',
color === 'gray' && 'bg-gray-200 text-gray-700 hover:bg-gray-300',
className
)}

View File

@ -18,10 +18,12 @@ export const useContract = (contractId: string) => {
return result.isLoading ? undefined : result.data
}
export const useContractWithPreload = (initial: Contract | null) => {
const [contract, setContract] = useStateCheckEquality<Contract | null>(
initial
)
export const useContractWithPreload = (
initial: Contract | null | undefined
) => {
const [contract, setContract] = useStateCheckEquality<
Contract | null | undefined
>(initial)
const contractId = initial?.id
useEffect(() => {

View File

@ -1,9 +1,7 @@
import { isEqual, sortBy } from 'lodash'
import { useEffect, useState } from 'react'
import { Fold } from 'common/fold'
import { User } from 'common/user'
import {
listAllFolds,
listenForFold,
listenForFolds,
listenForFoldsWithTags,
@ -80,38 +78,3 @@ export const useFollowedFoldIds = (user: User | null | undefined) => {
return followedFoldIds
}
// We also cache followedFolds directly in JSON.
// TODO: Extract out localStorage caches to a utility
export const useFollowedFolds = (user: User | null | undefined) => {
const [followedFolds, setFollowedFolds] = useState<Fold[] | undefined>()
const ids = useFollowedFoldIds(user)
useEffect(() => {
if (user && ids) {
const key = `followed-full-folds-${user.id}`
const followedFoldJson = localStorage.getItem(key)
if (followedFoldJson) {
setFollowedFolds(JSON.parse(followedFoldJson))
// Exit early if ids and followedFoldIds have all the same elements.
if (
isEqual(
sortBy(ids),
sortBy(JSON.parse(followedFoldJson).map((f: Fold) => f.id))
)
) {
return
}
}
// Otherwise, fetch the full contents of all folds
listAllFolds().then((folds) => {
const followedFolds = folds.filter((fold) => ids.includes(fold.id))
setFollowedFolds(followedFolds)
localStorage.setItem(key, JSON.stringify(followedFolds))
})
}
}, [user, ids])
return followedFolds
}

View File

@ -1,7 +1,7 @@
import { collection, query, where } from 'firebase/firestore'
import { Notification } from 'common/notification'
import { db } from 'web/lib/firebase/init'
import { getValues, listenForValues } from 'web/lib/firebase/utils'
import { listenForValues } from 'web/lib/firebase/utils'
function getNotificationsQuery(userId: string, unseenOnly?: boolean) {
const notifsCollection = collection(db, `/users/${userId}/notifications`)

View File

@ -39,6 +39,7 @@ import { FeedBet } from 'web/components/feed/feed-bets'
import { useIsIframe } from 'web/hooks/use-is-iframe'
import ContractEmbedPage from '../embed/[username]/[contractSlug]'
import { useBets } from 'web/hooks/use-bets'
import { AlertBox } from 'web/components/alert-box'
export const getStaticProps = fromPropz(getStaticPropz)
export async function getStaticPropz(props: {
@ -86,13 +87,13 @@ export default function ContractPage(props: {
slug: '',
}
const contract = useContractWithPreload(props.contract)
const inIframe = useIsIframe()
if (inIframe) {
return <ContractEmbedPage {...props} />
}
const { contract } = props
if (!contract) {
return <Custom404 />
}
@ -103,7 +104,9 @@ export default function ContractPage(props: {
export function ContractPageContent(
props: Parameters<typeof ContractPage>[0] & { contract: Contract }
) {
const { contract, backToHome, comments } = props
const { backToHome, comments } = props
const contract = useContractWithPreload(props.contract) ?? props.contract
const bets = useBets(contract.id) ?? props.bets
// Sort for now to see if bug is fixed.
@ -187,6 +190,12 @@ export function ContractPageContent(
bets={bets}
comments={comments ?? []}
/>
{isNumeric && (
<AlertBox
title="Warning"
text="Numeric markets were introduced as an experimental feature and are now deprecated."
/>
)}
{outcomeType === 'FREE_RESPONSE' && (
<>

View File

@ -37,7 +37,7 @@ export default function Activity() {
return (
<>
<Page assertUser="signed-in" suspend={!!contract}>
<Page suspend={!!contract}>
<Col className="mx-auto w-full max-w-[700px]">
<CategorySelector category={category} setCategory={setCategory} />
<Spacer h={1} />

View File

@ -16,7 +16,11 @@ export default function AddFundsPage() {
return (
<Page>
<SEO title="Add funds" description="Add funds" url="/add-funds" />
<SEO
title="Get Manifold Dollars"
description="Get Manifold Dollars"
url="/add-funds"
/>
<Col className="items-center">
<Col className="h-full rounded bg-white p-4 py-8 sm:p-8 sm:shadow-md">
@ -29,8 +33,8 @@ export default function AddFundsPage() {
/>
<div className="mb-6 text-gray-500">
Use Manifold Dollars to trade in your favorite markets. <br /> (Not
redeemable for cash.)
Purchase Manifold Dollars to trade in your favorite markets. <br />{' '}
(Not redeemable for cash.)
</div>
<div className="mb-2 text-sm text-gray-500">Amount</div>
@ -54,7 +58,7 @@ export default function AddFundsPage() {
>
<button
type="submit"
className="btn btn-primary w-full bg-gradient-to-r from-teal-500 to-green-500 font-medium hover:from-teal-600 hover:to-green-600"
className="btn btn-primary w-full bg-gradient-to-r from-indigo-500 to-blue-500 font-medium hover:from-indigo-600 hover:to-blue-600"
>
Checkout
</button>

View File

@ -19,6 +19,7 @@ export default async function handler(
listAllComments(contractId),
])
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const bets = allBets.map(({ userId, ...bet }) => bet) as Exclude<
Bet,
'userId'

View File

@ -24,6 +24,7 @@ export default async function handler(
listAllComments(contract.id),
])
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const bets = allBets.map(({ userId, ...bet }) => bet) as Exclude<
Bet,
'userId'

View File

@ -1,4 +1,12 @@
import { mapValues, groupBy, sumBy, sum, sortBy, debounce } from 'lodash'
import {
mapValues,
groupBy,
sumBy,
sum,
sortBy,
debounce,
uniqBy,
} from 'lodash'
import { useState, useMemo } from 'react'
import { charities, Charity as CharityType } from 'common/charity'
import { CharityCard } from 'web/components/charity/charity-card'
@ -8,7 +16,9 @@ import { Page } from 'web/components/page'
import { SiteLink } from 'web/components/site-link'
import { Title } from 'web/components/title'
import { getAllCharityTxns } from 'web/lib/firebase/txns'
import { formatMoney } from 'common/util/format'
import { manaToUSD } from 'common/util/format'
import { quadraticMatches } from 'common/quadratic-funding'
import { Txn } from 'common/txn'
export async function getStaticProps() {
const txns = await getAllCharityTxns()
@ -20,21 +30,55 @@ export async function getStaticProps() {
(charity) => (charity.tags?.includes('Featured') ? 0 : 1),
(charity) => -totals[charity.id],
])
const matches = quadraticMatches(txns, totalRaised)
const numDonors = uniqBy(txns, (txn) => txn.fromId).length
return {
props: {
totalRaised,
charities: sortedCharities,
matches,
txns,
numDonors,
},
revalidate: 60,
}
}
type Stat = {
name: string
stat: string
}
function DonatedStats(props: { stats: Stat[] }) {
const { stats } = props
return (
<dl className="mt-3 grid grid-cols-1 gap-5 rounded-lg bg-gradient-to-r from-pink-300 via-purple-300 to-indigo-400 p-4 sm:grid-cols-3">
{stats.map((item) => (
<div
key={item.name}
className="overflow-hidden rounded-lg bg-white px-4 py-5 shadow sm:p-6"
>
<dt className="truncate text-sm font-medium text-gray-500">
{item.name}
</dt>
<dd className="mt-1 text-3xl font-semibold text-gray-900">
{item.stat}
</dd>
</div>
))}
</dl>
)
}
export default function Charity(props: {
totalRaised: number
charities: CharityType[]
matches: { [charityId: string]: number }
txns: Txn[]
numDonors: number
}) {
const { totalRaised, charities } = props
const { totalRaised, charities, matches, numDonors } = props
const [query, setQuery] = useState('')
const debouncedQuery = debounce(setQuery, 50)
@ -54,26 +98,51 @@ export default function Charity(props: {
return (
<Page>
<Col className="w-full rounded px-4 py-6 sm:px-8 xl:w-[125%]">
<Col className="max-w-xl gap-2">
<Col className="">
<Title className="!mt-0" text="Manifold for Charity" />
<div className="mb-6 text-gray-500">
Donate your winnings to charity! Every {formatMoney(100)} you give
turns into $1 USD we send to your chosen charity.
<Spacer h={5} />
Together we've donated over ${Math.floor(totalRaised / 100)} USD so
far!
</div>
<span className="text-gray-600">
Through July 15, up to $25k of donations will be matched via{' '}
<SiteLink href="https://wtfisqf.com/" className="font-bold">
quadratic funding
</SiteLink>
, courtesy of{' '}
<SiteLink href="https://ftxfuturefund.org/" className="font-bold">
the FTX Future Fund
</SiteLink>
!
</span>
<DonatedStats
stats={[
{
name: 'Raised by Manifold users',
stat: manaToUSD(totalRaised),
},
{
name: 'Number of donors',
stat: `${numDonors}`,
},
{
name: 'Matched via quadratic funding',
stat: manaToUSD(sum(Object.values(matches))),
},
]}
/>
<Spacer h={10} />
<input
type="text"
onChange={(e) => debouncedQuery(e.target.value)}
placeholder="Search charities"
placeholder="Find a charity"
className="input input-bordered mb-6 w-full"
/>
</Col>
<div className="grid max-w-xl grid-flow-row grid-cols-1 gap-4 lg:max-w-full lg:grid-cols-2 xl:grid-cols-3">
{filterCharities.map((charity) => (
<CharityCard charity={charity} key={charity.name} />
<CharityCard
charity={charity}
key={charity.name}
match={matches[charity.id]}
/>
))}
</div>
{filterCharities.length === 0 && (
@ -82,32 +151,28 @@ export default function Charity(props: {
</div>
)}
<iframe
height="405"
src="https://manifold.markets/embed/ManifoldMarkets/total-donations-for-manifold-for-go"
title="Total donations for Manifold for Charity this May (in USD)"
frameBorder="0"
className="m-10 w-full rounded-xl bg-white p-10"
></iframe>
<div className="mt-10 w-full rounded-xl bg-gradient-to-r from-pink-300 via-purple-300 to-indigo-400 p-5">
<iframe
height="405"
src="https://manifold.markets/ManifoldMarkets/how-much-will-be-donated-through-ma"
title="Total donations for Manifold for Charity this May (in USD)"
frameBorder="0"
className="w-full rounded-xl bg-white p-10"
></iframe>
</div>
<div className="mt-10 text-gray-500">
Don't see your favorite charity? Recommend it{' '}
<SiteLink
href="https://manifold.markets/Sinclair/which-charities-should-manifold-add"
className="text-indigo-700"
>
here
</SiteLink>
!
<span className="font-semibold">Notes</span>
<br />
- Don't see your favorite charity? Recommend it by emailing
charity@manifold.markets!
<br />
<span className="italic">
Note: Manifold is not affiliated with non-Featured charities; we're
just fans of their work!
<br />
As Manifold is a for-profit entity, your contributions will not be
tax deductible.
</span>
- Manifold is not affiliated with non-Featured charities; we're just
fans of their work.
<br />
- As Manifold itself is a for-profit entity, your contributions will
not be tax deductible.
<br />- Donations + matches are wired once each quarter.
</div>
</Col>
</Page>

View File

@ -56,8 +56,8 @@ export default function Create() {
}
// Allow user to create a new contract
export function NewContract(props: { question: string; tag?: string }) {
const { question, tag } = props
export function NewContract(props: { question: string }) {
const { question } = props
const creator = useUser()
useEffect(() => {
@ -74,7 +74,7 @@ export function NewContract(props: { question: string; tag?: string }) {
// const [tagText, setTagText] = useState<string>(tag ?? '')
// const tags = parseWordsAsTags(tagText)
const [ante, setAnte] = useState(FIXED_ANTE)
const [ante, _setAnte] = useState(FIXED_ANTE)
const mustWaitForDailyFreeMarketStatus = useHasCreatedContractToday(creator)
@ -348,7 +348,7 @@ export function NewContract(props: { question: string; tag?: string }) {
className="btn btn-xs btn-primary"
onClick={() => (window.location.href = '/add-funds')}
>
Add funds
Get M$
</button>
</div>
)}

View File

@ -27,10 +27,8 @@ import { EditFoldButton } from 'web/components/folds/edit-fold-button'
import Custom404 from '../../404'
import { FollowFoldButton } from 'web/components/folds/follow-fold-button'
import { SEO } from 'web/components/SEO'
import { useTaggedContracts } from 'web/hooks/use-contracts'
import { Linkify } from 'web/components/linkify'
import { fromPropz, usePropz } from 'web/hooks/use-propz'
import { filterDefined } from 'common/util/array'
import { findActiveContracts } from 'web/components/feed/find-active-contracts'
import { Tabs } from 'web/components/layout/tabs'
@ -133,15 +131,6 @@ export default function FoldPage(props: {
const user = useUser()
const isCurator = user && fold && user.id === fold.curatorId
const taggedContracts = useTaggedContracts(fold?.tags) ?? props.contracts
const contractsMap = Object.fromEntries(
taggedContracts.map((contract) => [contract.id, contract])
)
const contracts = filterDefined(
props.contracts.map((contract) => contractsMap[contract.id])
)
if (fold === null || !foldSubpages.includes(page) || slugs[2]) {
return <Custom404 />
}

View File

@ -23,7 +23,7 @@ const Home = () => {
return (
<>
<Page assertUser="signed-in" suspend={!!contract}>
<Page suspend={!!contract}>
<Col className="mx-auto w-full p-2">
<ContractSearch
querySortOptions={{

View File

@ -3,7 +3,7 @@ import Router from 'next/router'
import { Contract, getContractsBySlugs } from 'web/lib/firebase/contracts'
import { Page } from 'web/components/page'
import { FeedPromo } from 'web/components/feed-create'
import { LandingPagePanel } from 'web/components/landing-page-panel'
import { Col } from 'web/components/layout/col'
import { useUser } from 'web/hooks/use-user'
import { ManifoldLogo } from 'web/components/nav/manifold-logo'
@ -40,13 +40,13 @@ const Home = (props: { hotContracts: Contract[] }) => {
}
return (
<Page assertUser="signed-out">
<Page>
<div className="px-4 pt-2 md:mt-0 lg:hidden">
<ManifoldLogo />
</div>
<Col className="items-center">
<Col className="max-w-3xl">
<FeedPromo hotContracts={hotContracts ?? []} />
<LandingPagePanel hotContracts={hotContracts ?? []} />
{/* <p className="mt-6 text-gray-500">
View{' '}
<SiteLink href="/markets" className="font-bold text-gray-700">

View File

@ -1,168 +0,0 @@
import React from 'react'
import {
LightningBoltIcon,
ScaleIcon,
UserCircleIcon,
BeakerIcon,
ArrowDownIcon,
} from '@heroicons/react/outline'
import { firebaseLogin } from 'web/lib/firebase/users'
import { ContractsGrid } from 'web/components/contract/contracts-list'
import { Col } from 'web/components/layout/col'
import Link from 'next/link'
import { Contract } from 'web/lib/firebase/contracts'
export default function LandingPage(props: { hotContracts: Contract[] }) {
const { hotContracts } = props
return (
<div>
<Hero />
<FeaturesSection />
{/* <ExploreMarketsSection hotContracts={hotContracts} /> */}
</div>
)
}
const scrollToAbout = () => {
const aboutElem = document.getElementById('about')
window.scrollTo({ top: aboutElem?.offsetTop, behavior: 'smooth' })
}
function Hero() {
return (
<div className="bg-world-trading h-screen overflow-hidden bg-gray-900 bg-cover bg-center lg:bg-left">
<main>
<div className="pt-32 sm:pt-8 lg:overflow-hidden lg:pt-0 lg:pb-14">
<div className="mx-auto max-w-7xl lg:px-8 xl:px-0">
<div className="lg:grid lg:grid-cols-2 lg:gap-8">
<div className="mx-auto max-w-md px-8 sm:max-w-2xl sm:text-center lg:flex lg:items-center lg:px-0 lg:text-left">
<div className="lg:py-24">
<h1 className="mt-4 text-4xl text-white sm:mt-5 sm:text-6xl lg:mt-6 xl:text-6xl">
<div className="mb-2">Create your own</div>
<div className="bg-gradient-to-r from-teal-300 to-green-400 bg-clip-text font-bold text-transparent">
prediction markets
</div>
</h1>
<p className="mt-3 text-base text-white sm:mt-5 sm:text-xl lg:text-lg xl:text-xl">
Better forecasting through accessible prediction markets
<br />
for you and your community
</p>
<div className="mt-10 sm:mt-12">
<button
className="btn bg-gradient-to-r from-teal-500 to-green-500 px-10 text-lg font-medium normal-case hover:from-teal-600 hover:to-green-600"
onClick={firebaseLogin}
>
Sign in to get started!
</button>
</div>
</div>
</div>
</div>
</div>
<div className="absolute bottom-12 w-full">
<ArrowDownIcon
className="mx-auto animate-bounce cursor-pointer text-white"
width={32}
height={32}
onClick={scrollToAbout}
/>
</div>
</div>
</main>
</div>
)
}
function FeaturesSection() {
const features = [
{
name: 'Easy to participate',
description: 'Sign up for free and make your own predictions in seconds!',
icon: UserCircleIcon,
},
{
name: 'Play money, real results',
description:
'Get accurate predictions by betting with Manifold Dollars, our virtual currency.',
icon: LightningBoltIcon,
},
{
name: 'Creator-driven markets',
description:
'Resolve markets you create with your own judgment—enabling new markets with subjective or personal questions.',
icon: ScaleIcon,
},
{
name: 'Become smarter',
description:
'Bet on questions that matter and share the forecasts. With better information, we can all make better decisions.',
icon: BeakerIcon,
},
]
return (
<div id="about" className="w-full bg-green-50 py-16">
<div className="mx-auto max-w-4xl py-12">
<div className="mx-auto max-w-7xl px-6 lg:px-8">
<div className="lg:text-center">
<h2 className="text-base font-semibold uppercase tracking-wide text-teal-600">
Manifold Markets
</h2>
<p className="mt-2 text-3xl font-extrabold leading-8 tracking-tight text-gray-900 sm:text-4xl">
Better forecasting for everyone
</p>
<p className="mt-4 max-w-2xl text-xl text-gray-500 lg:mx-auto">
The easiest way to get an accurate forecast on anything
</p>
</div>
<div className="mt-10">
<dl className="space-y-10 md:grid md:grid-cols-2 md:gap-x-8 md:gap-y-10 md:space-y-0">
{features.map((feature) => (
<div key={feature.name} className="relative">
<dt>
<div className="absolute flex h-12 w-12 items-center justify-center rounded-md bg-teal-500 text-white">
<feature.icon className="h-6 w-6" aria-hidden="true" />
</div>
<p className="ml-16 text-lg font-medium leading-6 text-gray-900">
{feature.name}
</p>
</dt>
<dd className="mt-2 ml-16 text-base text-gray-500">
{feature.description}
</dd>
</div>
))}
</dl>
</div>
</div>
<Col className="mt-20">
<Link href="/about">
<a className="btn btn-primary mx-auto">Learn more</a>
</Link>
</Col>
</div>
</div>
)
}
function ExploreMarketsSection(props: { hotContracts: Contract[] }) {
const { hotContracts } = props
return (
<div className="mx-auto max-w-4xl px-4 py-8">
<p className="my-12 text-3xl font-extrabold leading-8 tracking-tight text-indigo-700 sm:text-4xl">
Today's top markets
</p>
<ContractsGrid
contracts={hotContracts}
loadMore={() => {}}
hasMore={false}
/>
</div>
)
}

View File

@ -8,8 +8,8 @@ import { fromPropz, usePropz } from 'web/hooks/use-propz'
export const getStaticProps = fromPropz(getStaticPropz)
export async function getStaticPropz() {
const [topTraders, topCreators] = await Promise.all([
getTopTraders().catch((_) => {}),
getTopCreators().catch((_) => {}),
getTopTraders().catch(() => {}),
getTopCreators().catch(() => {}),
])
return {

View File

@ -290,13 +290,3 @@ ${TEST_VALUE}
</Page>
)
}
// Given a date string like '2022-04-02',
// return the time just before midnight on that date (in the user's local time), as millis since epoch
function dateToMillis(date: string) {
return dayjs(date)
.set('hour', 23)
.set('minute', 59)
.set('second', 59)
.valueOf()
}

View File

@ -31,9 +31,7 @@ import { Linkify } from 'web/components/linkify'
import {
BinaryOutcomeLabel,
CancelLabel,
FreeResponseOutcomeLabel,
MultiLabel,
OutcomeLabel,
ProbPercentLabel,
} from 'web/components/outcome-label'
import {
@ -44,7 +42,7 @@ import {
import { getContractFromId } from 'web/lib/firebase/contracts'
import { CheckIcon, XIcon } from '@heroicons/react/outline'
import toast from 'react-hot-toast'
import { formatMoney, formatPercent } from 'common/util/format'
import { formatMoney } from 'common/util/format'
export default function Notifications() {
const user = useUser()
@ -184,7 +182,7 @@ function NotificationGroupItem(props: {
className?: string
}) {
const { notificationGroup, className } = props
const { sourceContractId, notifications, timePeriod } = notificationGroup
const { sourceContractId, notifications } = notificationGroup
const {
sourceContractTitle,
sourceContractSlug,

View File

@ -1,6 +1,5 @@
import dayjs from 'dayjs'
import { zip, uniq, sumBy, concat, countBy, sortBy, sum } from 'lodash'
import { IS_PRIVATE_MANIFOLD } from 'common/envs/constants'
import {
DailyCountChart,
DailyPercentChart,
@ -158,13 +157,11 @@ export async function getStaticPropz() {
bets?.map((b) => b.userId) ?? [],
comments?.map((c) => c.userId) ?? []
)
const counts = Object.entries(countBy(userIds))
const topTenth = sortBy(counts, ([, count]) => count)
.reverse()
// Take the top 10% of users, except for the top 2, to avoid outliers.
.slice(2, counts.length * 0.1)
const topTenthTotal = sumBy(topTenth, ([_, count]) => count)
return topTenthTotal
const counts = Object.values(countBy(userIds))
const sortedCounts = sortBy(counts, (count) => count).reverse()
if (sortedCounts.length === 0) return 0
const tenthPercentile = sortedCounts[Math.floor(sortedCounts.length * 0.1)]
return tenthPercentile
})
const weeklyTopTenthActions = dailyTopTenthActions.map((_, i) => {
const start = Math.max(0, i - 6)
@ -213,9 +210,11 @@ export async function getStaticPropz() {
weekOnWeekRetention,
weeklyActivationRate,
monthlyRetention,
dailyTopTenthActions,
weeklyTopTenthActions,
monthlyTopTenthActions,
topTenthActions: {
daily: dailyTopTenthActions,
weekly: weeklyTopTenthActions,
monthly: monthlyTopTenthActions,
},
manaBet: {
daily: dailyManaBet,
weekly: weeklyManaBet,
@ -238,9 +237,11 @@ export default function Analytics(props: {
weekOnWeekRetention: number[]
monthlyRetention: number[]
weeklyActivationRate: number[]
dailyTopTenthActions: number[]
weeklyTopTenthActions: number[]
monthlyTopTenthActions: number[]
topTenthActions: {
daily: number[]
weekly: number[]
monthly: number[]
}
manaBet: {
daily: number[]
weekly: number[]
@ -259,9 +260,11 @@ export default function Analytics(props: {
weekOnWeekRetention: [],
monthlyRetention: [],
weeklyActivationRate: [],
dailyTopTenthActions: [],
weeklyTopTenthActions: [],
monthlyTopTenthActions: [],
topTenthActions: {
daily: [],
weekly: [],
monthly: [],
},
manaBet: {
daily: [],
weekly: [],
@ -302,9 +305,11 @@ export function CustomAnalytics(props: {
weekOnWeekRetention: number[]
monthlyRetention: number[]
weeklyActivationRate: number[]
dailyTopTenthActions: number[]
weeklyTopTenthActions: number[]
monthlyTopTenthActions: number[]
topTenthActions: {
daily: number[]
weekly: number[]
monthly: number[]
}
manaBet: {
daily: number[]
weekly: number[]
@ -322,9 +327,7 @@ export function CustomAnalytics(props: {
weekOnWeekRetention,
monthlyRetention,
weeklyActivationRate,
dailyTopTenthActions,
weeklyTopTenthActions,
monthlyTopTenthActions,
topTenthActions,
manaBet,
} = props
@ -526,10 +529,10 @@ export function CustomAnalytics(props: {
/>
<Spacer h={8} />
<Title text="Total actions by top tenth" />
<Title text="Action count of top tenth" />
<p className="text-gray-500">
From the top 10% of users, how many bets, comments, and markets did they
create? (Excluding top 2 users each day.)
Number of actions (bets, comments, markets created) taken by the tenth
percentile of top users.
</p>
<Tabs
defaultIndex={1}
@ -538,7 +541,7 @@ export function CustomAnalytics(props: {
title: 'Daily',
content: (
<DailyCountChart
dailyCounts={dailyTopTenthActions}
dailyCounts={topTenthActions.daily}
startDate={startDate}
small
/>
@ -548,7 +551,7 @@ export function CustomAnalytics(props: {
title: 'Weekly',
content: (
<DailyCountChart
dailyCounts={weeklyTopTenthActions}
dailyCounts={topTenthActions.weekly}
startDate={startDate}
small
/>
@ -558,7 +561,7 @@ export function CustomAnalytics(props: {
title: 'Monthly',
content: (
<DailyCountChart
dailyCounts={monthlyTopTenthActions}
dailyCounts={topTenthActions.monthly}
startDate={startDate}
small
/>

View File

@ -6,8 +6,8 @@ import { Title } from '../../components/title'
export default function TagPage() {
const router = useRouter()
const { tag } = router.query as { tag: string }
if (!router.isReady) return <div />
// TODO: Fix error: The provided `href` (/tag/[tag]?s=newest) value is missing query values (tag)
return (
<Page>
<Title text={`#${tag}`} />

Binary file not shown.

Before

Width:  |  Height:  |  Size: 287 KiB

After

Width:  |  Height:  |  Size: 212 KiB