Merge branch 'main' into atlas
This commit is contained in:
commit
a1368abd66
27
common/quadratic-funding.ts
Normal file
27
common/quadratic-funding.ts
Normal 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))
|
||||||
|
}
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { sortBy } from 'lodash'
|
||||||
|
|
||||||
export const logInterpolation = (min: number, max: number, value: number) => {
|
export const logInterpolation = (min: number, max: number, value: number) => {
|
||||||
if (value <= min) return 0
|
if (value <= min) return 0
|
||||||
if (value >= max) return 1
|
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]
|
||||||
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@ import * as admin from 'firebase-admin'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
|
|
||||||
import { APIError, newEndpoint, validate } from './api'
|
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 { User } from '../../common/user'
|
||||||
import { getCpmmSellBetInfo } from '../../common/sell-bet'
|
import { getCpmmSellBetInfo } from '../../common/sell-bet'
|
||||||
import { addObjects, removeUndefinedProps } from '../../common/util/object'
|
import { addObjects, removeUndefinedProps } from '../../common/util/object'
|
||||||
|
@ -57,8 +57,12 @@ export const sellshares = newEndpoint(['POST'], async (req, auth) => {
|
||||||
prevLoanAmount
|
prevLoanAmount
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!isFinite(newP)) {
|
if (
|
||||||
throw new APIError(500, 'Trade rejected due to overflow error.')
|
!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()
|
const newBetDoc = firestore.collection(`contracts/${contractId}/bets`).doc()
|
||||||
|
|
|
@ -8,8 +8,15 @@ module.exports = {
|
||||||
],
|
],
|
||||||
rules: {
|
rules: {
|
||||||
'@typescript-eslint/no-empty-function': 'off',
|
'@typescript-eslint/no-empty-function': 'off',
|
||||||
'@typescript-eslint/no-unused-vars': 'off',
|
|
||||||
'@typescript-eslint/no-explicit-any': '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-img-element': 'off',
|
||||||
'@next/next/no-typos': 'off',
|
'@next/next/no-typos': 'off',
|
||||||
'lodash/import-scope': [2, 'member'],
|
'lodash/import-scope': [2, 'member'],
|
||||||
|
|
|
@ -24,7 +24,7 @@ export function AddFundsButton(props: { className?: string }) {
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
Add funds
|
Get M$
|
||||||
</label>
|
</label>
|
||||||
<input type="checkbox" id="add-funds" className="modal-toggle" />
|
<input type="checkbox" id="add-funds" className="modal-toggle" />
|
||||||
|
|
||||||
|
|
24
web/components/alert-box.tsx
Normal file
24
web/components/alert-box.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
import { sortBy, partition, sum, uniq } from 'lodash'
|
import { sortBy, partition, sum, uniq } from 'lodash'
|
||||||
import { useLayoutEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
|
|
||||||
import { FreeResponseContract } from 'common/contract'
|
import { FreeResponseContract } from 'common/contract'
|
||||||
import { Col } from '../layout/col'
|
import { Col } from '../layout/col'
|
||||||
|
@ -85,7 +85,7 @@ export function AnswersPanel(props: { contract: FreeResponseContract }) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useEffect(() => {
|
||||||
setChosenAnswers({})
|
setChosenAnswers({})
|
||||||
}, [resolveOption])
|
}, [resolveOption])
|
||||||
|
|
||||||
|
@ -116,7 +116,7 @@ export function AnswersPanel(props: { contract: FreeResponseContract }) {
|
||||||
{!resolveOption && (
|
{!resolveOption && (
|
||||||
<div className={clsx('flow-root pr-2 md:pr-0')}>
|
<div className={clsx('flow-root pr-2 md:pr-0')}>
|
||||||
<div className={clsx(tradingAllowed(contract) ? '' : '-mb-6')}>
|
<div className={clsx(tradingAllowed(contract) ? '' : '-mb-6')}>
|
||||||
{answerItems.map((item, activityItemIdx) => (
|
{answerItems.map((item) => (
|
||||||
<div key={item.id} className={'relative pb-2'}>
|
<div key={item.id} className={'relative pb-2'}>
|
||||||
<div className="relative flex items-start space-x-3">
|
<div className="relative flex items-start space-x-3">
|
||||||
<OpenAnswer {...item} />
|
<OpenAnswer {...item} />
|
||||||
|
|
|
@ -138,9 +138,8 @@ export function BetsList(props: { user: User; hideBetsBefore?: number }) {
|
||||||
return !hasSoldAll
|
return !hasSoldAll
|
||||||
})
|
})
|
||||||
|
|
||||||
const [settled, unsettled] = partition(
|
const unsettled = contracts.filter(
|
||||||
contracts,
|
(c) => !c.isResolved && contractsMetrics[c.id].invested !== 0
|
||||||
(c) => c.isResolved || contractsMetrics[c.id].invested === 0
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const currentInvested = sumBy(
|
const currentInvested = sumBy(
|
||||||
|
@ -261,7 +260,7 @@ function ContractBets(props: {
|
||||||
|
|
||||||
const isBinary = outcomeType === 'BINARY'
|
const isBinary = outcomeType === 'BINARY'
|
||||||
|
|
||||||
const { payout, profit, profitPercent, invested } = getContractBetMetrics(
|
const { payout, profit, profitPercent } = getContractBetMetrics(
|
||||||
contract,
|
contract,
|
||||||
bets
|
bets
|
||||||
)
|
)
|
||||||
|
@ -657,7 +656,6 @@ function SellButton(props: { contract: Contract; bet: Bet }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ConfirmationButton
|
<ConfirmationButton
|
||||||
id={`sell-${bet.id}`}
|
|
||||||
openModalBtn={{
|
openModalBtn={{
|
||||||
className: clsx('btn-sm', isSubmitting && 'btn-disabled loading'),
|
className: clsx('btn-sm', isSubmitting && 'btn-disabled loading'),
|
||||||
label: 'Sell',
|
label: 'Sell',
|
||||||
|
|
|
@ -6,9 +6,11 @@ import { Charity } from 'common/charity'
|
||||||
import { useCharityTxns } from 'web/hooks/use-charity-txns'
|
import { useCharityTxns } from 'web/hooks/use-charity-txns'
|
||||||
import { manaToUSD } from '../../../common/util/format'
|
import { manaToUSD } from '../../../common/util/format'
|
||||||
import { Row } from '../layout/row'
|
import { Row } from '../layout/row'
|
||||||
|
import { Col } from '../layout/col'
|
||||||
|
|
||||||
export function CharityCard(props: { charity: Charity }) {
|
export function CharityCard(props: { charity: Charity; match?: number }) {
|
||||||
const { name, slug, photo, preview, id, tags } = props.charity
|
const { charity, match } = props
|
||||||
|
const { slug, photo, preview, id, tags } = charity
|
||||||
|
|
||||||
const txns = useCharityTxns(id)
|
const txns = useCharityTxns(id)
|
||||||
const raised = sumBy(txns, (txn) => txn.amount)
|
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> */}
|
{/* <h3 className="card-title line-clamp-3">{name}</h3> */}
|
||||||
<div className="line-clamp-4 text-sm">{preview}</div>
|
<div className="line-clamp-4 text-sm">{preview}</div>
|
||||||
{raised > 0 && (
|
{raised > 0 && (
|
||||||
<Row className="text-primary mt-4 flex-1 items-end justify-center gap-2">
|
<>
|
||||||
<span className="text-3xl">
|
<Row className="mt-4 flex-1 items-end justify-center gap-6 text-gray-900">
|
||||||
{raised < 100
|
<Col>
|
||||||
? manaToUSD(raised)
|
<span className="text-3xl font-semibold">
|
||||||
: '$' + Math.floor(raised / 100)}
|
{formatUsd(raised)}
|
||||||
</span>
|
</span>
|
||||||
<span>raised</span>
|
<span>raised</span>
|
||||||
</Row>
|
</Col>
|
||||||
|
{match && (
|
||||||
|
<Col className="text-gray-500">
|
||||||
|
<span className="text-xl">+{formatUsd(match)}</span>
|
||||||
|
<span className="">match</span>
|
||||||
|
</Col>
|
||||||
|
)}
|
||||||
|
</Row>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</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() {
|
function FeaturedBadge() {
|
||||||
return (
|
return (
|
||||||
<span className="inline-flex items-center gap-1 bg-yellow-100 px-3 py-0.5 text-sm font-medium text-yellow-800">
|
<span className="inline-flex items-center gap-1 bg-yellow-100 px-3 py-0.5 text-sm font-medium text-yellow-800">
|
||||||
|
|
|
@ -24,13 +24,12 @@ export function ChoicesToggleGroup(props: {
|
||||||
<RadioGroup
|
<RadioGroup
|
||||||
className={clsx(className, 'flex flex-row flex-wrap items-center gap-3')}
|
className={clsx(className, 'flex flex-row flex-wrap items-center gap-3')}
|
||||||
value={currentChoice.toString()}
|
value={currentChoice.toString()}
|
||||||
onChange={(str) => null}
|
onChange={setChoice}
|
||||||
>
|
>
|
||||||
{Object.keys(choicesMap).map((choiceKey) => (
|
{Object.keys(choicesMap).map((choiceKey) => (
|
||||||
<RadioGroup.Option
|
<RadioGroup.Option
|
||||||
key={choiceKey}
|
key={choiceKey}
|
||||||
value={choicesMap[choiceKey]}
|
value={choicesMap[choiceKey]}
|
||||||
onClick={() => setChoice(choicesMap[choiceKey])}
|
|
||||||
className={({ active }) =>
|
className={({ active }) =>
|
||||||
clsx(
|
clsx(
|
||||||
active ? 'ring-2 ring-indigo-500 ring-offset-2' : '',
|
active ? 'ring-2 ring-indigo-500 ring-offset-2' : '',
|
||||||
|
|
|
@ -5,7 +5,6 @@ import { Modal } from './layout/modal'
|
||||||
import { Row } from './layout/row'
|
import { Row } from './layout/row'
|
||||||
|
|
||||||
export function ConfirmationButton(props: {
|
export function ConfirmationButton(props: {
|
||||||
id: string
|
|
||||||
openModalBtn: {
|
openModalBtn: {
|
||||||
label: string
|
label: string
|
||||||
icon?: JSX.Element
|
icon?: JSX.Element
|
||||||
|
@ -22,7 +21,7 @@ export function ConfirmationButton(props: {
|
||||||
onSubmit: () => void
|
onSubmit: () => void
|
||||||
children: ReactNode
|
children: ReactNode
|
||||||
}) {
|
}) {
|
||||||
const { id, openModalBtn, cancelBtn, submitBtn, onSubmit, children } = props
|
const { openModalBtn, cancelBtn, submitBtn, onSubmit, children } = props
|
||||||
|
|
||||||
const [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
|
|
||||||
|
@ -67,7 +66,6 @@ export function ResolveConfirmationButton(props: {
|
||||||
props
|
props
|
||||||
return (
|
return (
|
||||||
<ConfirmationButton
|
<ConfirmationButton
|
||||||
id="resolution-modal"
|
|
||||||
openModalBtn={{
|
openModalBtn={{
|
||||||
className: clsx(
|
className: clsx(
|
||||||
'border-none self-start',
|
'border-none self-start',
|
||||||
|
|
|
@ -140,7 +140,7 @@ export function QuickBet(props: { contract: Contract; user: User }) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const textColor = `text-${getColor(contract, previewProb)}`
|
const textColor = `text-${getColor(contract)}`
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Col
|
<Col
|
||||||
|
@ -223,7 +223,7 @@ export function QuickBet(props: { contract: Contract; user: User }) {
|
||||||
|
|
||||||
export function ProbBar(props: { contract: Contract; previewProb?: number }) {
|
export function ProbBar(props: { contract: Contract; previewProb?: number }) {
|
||||||
const { contract, previewProb } = props
|
const { contract, previewProb } = props
|
||||||
const color = getColor(contract, previewProb)
|
const color = getColor(contract)
|
||||||
const prob = previewProb ?? getProb(contract)
|
const prob = previewProb ?? getProb(contract)
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -257,7 +257,7 @@ function QuickOutcomeView(props: {
|
||||||
// If there's a preview prob, display that instead of the current prob
|
// If there's a preview prob, display that instead of the current prob
|
||||||
const override =
|
const override =
|
||||||
previewProb === undefined ? undefined : formatPercent(previewProb)
|
previewProb === undefined ? undefined : formatPercent(previewProb)
|
||||||
const textColor = `text-${getColor(contract, previewProb)}`
|
const textColor = `text-${getColor(contract)}`
|
||||||
|
|
||||||
let display: string | undefined
|
let display: string | undefined
|
||||||
switch (outcomeType) {
|
switch (outcomeType) {
|
||||||
|
@ -306,7 +306,7 @@ function getNumericScale(contract: NumericContract) {
|
||||||
return (ev - min) / (max - min)
|
return (ev - min) / (max - min)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getColor(contract: Contract, previewProb?: number) {
|
export function getColor(contract: Contract) {
|
||||||
// TODO: Try injecting a gradient here
|
// TODO: Try injecting a gradient here
|
||||||
// return 'primary'
|
// return 'primary'
|
||||||
const { resolution } = contract
|
const { resolution } = contract
|
||||||
|
|
|
@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -2,7 +2,7 @@ import { Answer } from 'common/answer'
|
||||||
import { Bet } from 'common/bet'
|
import { Bet } from 'common/bet'
|
||||||
import { Comment } from 'common/comment'
|
import { Comment } from 'common/comment'
|
||||||
import { formatPercent } from 'common/util/format'
|
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 { Col } from 'web/components/layout/col'
|
||||||
import { Modal } from 'web/components/layout/modal'
|
import { Modal } from 'web/components/layout/modal'
|
||||||
import { AnswerBetPanel } from 'web/components/answers/answer-bet-panel'
|
import { AnswerBetPanel } from 'web/components/answers/answer-bet-panel'
|
||||||
|
|
|
@ -3,7 +3,6 @@ import React, { useState } from 'react'
|
||||||
import {
|
import {
|
||||||
BanIcon,
|
BanIcon,
|
||||||
CheckIcon,
|
CheckIcon,
|
||||||
DotsVerticalIcon,
|
|
||||||
LockClosedIcon,
|
LockClosedIcon,
|
||||||
XIcon,
|
XIcon,
|
||||||
} from '@heroicons/react/solid'
|
} 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>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
|
@ -48,7 +48,6 @@ export function CreateFoldButton() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ConfirmationButton
|
<ConfirmationButton
|
||||||
id="create-fold"
|
|
||||||
openModalBtn={{
|
openModalBtn={{
|
||||||
label: 'New',
|
label: 'New',
|
||||||
icon: <PlusCircleIcon className="mr-2 h-5 w-5" />,
|
icon: <PlusCircleIcon className="mr-2 h-5 w-5" />,
|
||||||
|
|
65
web/components/landing-page-panel.tsx
Normal file
65
web/components/landing-page-panel.tsx
Normal 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}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
|
@ -98,7 +98,7 @@ function AddLiquidityPanel(props: { contract: CPMMContract }) {
|
||||||
setError('Server error')
|
setError('Server error')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((e) => setError('Server error'))
|
.catch((_) => setError('Server error'))
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -136,7 +136,7 @@ function AddLiquidityPanel(props: { contract: CPMMContract }) {
|
||||||
|
|
||||||
function ViewLiquidityPanel(props: { contract: CPMMContract }) {
|
function ViewLiquidityPanel(props: { contract: CPMMContract }) {
|
||||||
const { contract } = props
|
const { contract } = props
|
||||||
const { id: contractId, pool } = contract
|
const { pool } = contract
|
||||||
const { YES: yesShares, NO: noShares } = pool
|
const { YES: yesShares, NO: noShares } = pool
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -162,7 +162,7 @@ function WithdrawLiquidityPanel(props: {
|
||||||
const { contract, lpShares } = props
|
const { contract, lpShares } = props
|
||||||
const { YES: yesShares, NO: noShares } = lpShares
|
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 [isSuccess, setIsSuccess] = useState(false)
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
|
||||||
|
@ -171,12 +171,12 @@ function WithdrawLiquidityPanel(props: {
|
||||||
setIsSuccess(false)
|
setIsSuccess(false)
|
||||||
|
|
||||||
withdrawLiquidity({ contractId: contract.id })
|
withdrawLiquidity({ contractId: contract.id })
|
||||||
.then((r) => {
|
.then((_) => {
|
||||||
setIsSuccess(true)
|
setIsSuccess(true)
|
||||||
setError(undefined)
|
setError(undefined)
|
||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
})
|
})
|
||||||
.catch((e) => setError('Server error'))
|
.catch((_) => setError('Server error'))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isSuccess)
|
if (isSuccess)
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
import Link from 'next/link'
|
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 { formatMoney } from 'common/util/format'
|
||||||
import { Avatar } from '../avatar'
|
import { Avatar } from '../avatar'
|
||||||
import { IS_PRIVATE_MANIFOLD } from 'common/envs/constants'
|
|
||||||
|
|
||||||
export function ProfileSummary(props: { user: User }) {
|
export function ProfileSummary(props: { user: User }) {
|
||||||
const { user } = props
|
const { user } = props
|
||||||
|
|
|
@ -7,15 +7,12 @@ import {
|
||||||
CashIcon,
|
CashIcon,
|
||||||
HeartIcon,
|
HeartIcon,
|
||||||
PresentationChartLineIcon,
|
PresentationChartLineIcon,
|
||||||
ChatAltIcon,
|
|
||||||
SparklesIcon,
|
SparklesIcon,
|
||||||
NewspaperIcon,
|
NewspaperIcon,
|
||||||
} from '@heroicons/react/outline'
|
} from '@heroicons/react/outline'
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
import { sortBy } from 'lodash'
|
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
import { useFollowedFolds } from 'web/hooks/use-fold'
|
|
||||||
import { useUser } from 'web/hooks/use-user'
|
import { useUser } from 'web/hooks/use-user'
|
||||||
import { firebaseLogin, firebaseLogout, User } from 'web/lib/firebase/users'
|
import { firebaseLogin, firebaseLogout, User } from 'web/lib/firebase/users'
|
||||||
import { ManifoldLogo } from './manifold-logo'
|
import { ManifoldLogo } from './manifold-logo'
|
||||||
|
@ -51,7 +48,7 @@ function getNavigation(username: string) {
|
||||||
icon: NotificationsIcon,
|
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) {
|
if (!user) {
|
||||||
return [
|
return [
|
||||||
{ name: 'Leaderboards', href: '/leaderboards' },
|
{ name: 'Leaderboards', href: '/leaderboards' },
|
||||||
|
{ name: 'Charity', href: '/charity' },
|
||||||
{ name: 'Discord', href: 'https://discord.gg/eHQBNBqXuh' },
|
{ name: 'Discord', href: 'https://discord.gg/eHQBNBqXuh' },
|
||||||
{ name: 'Twitter', href: 'https://twitter.com/ManifoldMarkets' },
|
{ name: 'Twitter', href: 'https://twitter.com/ManifoldMarkets' },
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{ name: 'Add funds', href: '/add-funds' },
|
|
||||||
{ name: 'Leaderboards', href: '/leaderboards' },
|
{ name: 'Leaderboards', href: '/leaderboards' },
|
||||||
|
{ name: 'Charity', href: '/charity' },
|
||||||
{ name: 'Blog', href: 'https://news.manifold.markets' },
|
{ name: 'Blog', href: 'https://news.manifold.markets' },
|
||||||
{ name: 'Discord', href: 'https://discord.gg/eHQBNBqXuh' },
|
{ name: 'Discord', href: 'https://discord.gg/eHQBNBqXuh' },
|
||||||
{ name: 'Twitter', href: 'https://twitter.com/ManifoldMarkets' },
|
{ name: 'Twitter', href: 'https://twitter.com/ManifoldMarkets' },
|
||||||
|
@ -104,7 +102,7 @@ const signedOutMobileNavigation = [
|
||||||
]
|
]
|
||||||
|
|
||||||
const mobileNavigation = [
|
const mobileNavigation = [
|
||||||
{ name: 'Add funds', href: '/add-funds', icon: CashIcon },
|
{ name: 'Get M$', href: '/add-funds', icon: CashIcon },
|
||||||
...signedOutMobileNavigation,
|
...signedOutMobileNavigation,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -179,8 +177,6 @@ export default function Sidebar(props: { className?: string }) {
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const user = useUser()
|
const user = useUser()
|
||||||
let folds = useFollowedFolds(user) || []
|
|
||||||
folds = sortBy(folds, 'followCount').reverse()
|
|
||||||
const mustWaitForFreeMarketStatus = useHasCreatedContractToday(user)
|
const mustWaitForFreeMarketStatus = useHasCreatedContractToday(user)
|
||||||
const navigationOptions =
|
const navigationOptions =
|
||||||
user === null
|
user === null
|
||||||
|
|
|
@ -53,7 +53,6 @@ function NumericBuyPanel(props: {
|
||||||
|
|
||||||
const [betAmount, setBetAmount] = useState<number | undefined>(undefined)
|
const [betAmount, setBetAmount] = useState<number | undefined>(undefined)
|
||||||
|
|
||||||
const [valueError, setValueError] = useState<string | undefined>()
|
|
||||||
const [error, setError] = useState<string | undefined>()
|
const [error, setError] = useState<string | undefined>()
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||||
const [wasSubmitted, setWasSubmitted] = useState(false)
|
const [wasSubmitted, setWasSubmitted] = useState(false)
|
||||||
|
|
|
@ -6,12 +6,11 @@ import { Toaster } from 'react-hot-toast'
|
||||||
|
|
||||||
export function Page(props: {
|
export function Page(props: {
|
||||||
margin?: boolean
|
margin?: boolean
|
||||||
assertUser?: 'signed-in' | 'signed-out'
|
|
||||||
rightSidebar?: ReactNode
|
rightSidebar?: ReactNode
|
||||||
suspend?: boolean
|
suspend?: boolean
|
||||||
children?: ReactNode
|
children?: ReactNode
|
||||||
}) {
|
}) {
|
||||||
const { margin, assertUser, children, rightSidebar, suspend } = props
|
const { margin, children, rightSidebar, suspend } = props
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
|
@ -1,4 +1,8 @@
|
||||||
import clsx from 'clsx'
|
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 { follow, unfollow, User } from 'web/lib/firebase/users'
|
||||||
import { CreatorContractsList } from './contract/contracts-list'
|
import { CreatorContractsList } from './contract/contracts-list'
|
||||||
import { SEO } from './SEO'
|
import { SEO } from './SEO'
|
||||||
|
@ -9,9 +13,7 @@ import { Col } from './layout/col'
|
||||||
import { Linkify } from './linkify'
|
import { Linkify } from './linkify'
|
||||||
import { Spacer } from './layout/spacer'
|
import { Spacer } from './layout/spacer'
|
||||||
import { Row } from './layout/row'
|
import { Row } from './layout/row'
|
||||||
import { LinkIcon } from '@heroicons/react/solid'
|
|
||||||
import { genHash } from 'common/util/random'
|
import { genHash } from 'common/util/random'
|
||||||
import { PencilIcon } from '@heroicons/react/outline'
|
|
||||||
import { Tabs } from './layout/tabs'
|
import { Tabs } from './layout/tabs'
|
||||||
import { UserCommentsList } from './comments-list'
|
import { UserCommentsList } from './comments-list'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
|
@ -22,8 +24,10 @@ import { LoadingIndicator } from './loading-indicator'
|
||||||
import { BetsList } from './bets-list'
|
import { BetsList } from './bets-list'
|
||||||
import { Bet } from 'common/bet'
|
import { Bet } from 'common/bet'
|
||||||
import { getUserBets } from 'web/lib/firebase/bets'
|
import { getUserBets } from 'web/lib/firebase/bets'
|
||||||
import { uniq } from 'lodash'
|
|
||||||
import { FollowersButton, FollowingButton } from './following-button'
|
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: {
|
export function UserLink(props: {
|
||||||
name: string
|
name: string
|
||||||
|
@ -305,29 +309,3 @@ export function defaultBannerUrl(userId: string) {
|
||||||
]
|
]
|
||||||
return defaultBanner[genHash(userId)() % defaultBanner.length]
|
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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
|
@ -168,7 +168,7 @@ export function FundsSelector(props: {
|
||||||
{fundAmounts.map((amount) => (
|
{fundAmounts.map((amount) => (
|
||||||
<Button
|
<Button
|
||||||
key={amount}
|
key={amount}
|
||||||
color={selected === amount ? 'green' : 'gray'}
|
color={selected === amount ? 'indigo' : 'gray'}
|
||||||
onClick={() => onSelect(amount as any)}
|
onClick={() => onSelect(amount as any)}
|
||||||
className={btnClassName}
|
className={btnClassName}
|
||||||
>
|
>
|
||||||
|
@ -230,7 +230,7 @@ export function NumberCancelSelector(props: {
|
||||||
function Button(props: {
|
function Button(props: {
|
||||||
className?: string
|
className?: string
|
||||||
onClick?: () => void
|
onClick?: () => void
|
||||||
color: 'green' | 'red' | 'blue' | 'yellow' | 'gray'
|
color: 'green' | 'red' | 'blue' | 'indigo' | 'yellow' | 'gray'
|
||||||
children?: ReactNode
|
children?: ReactNode
|
||||||
}) {
|
}) {
|
||||||
const { className, onClick, children, color } = props
|
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 === 'red' && 'bg-red-400 text-white hover:bg-red-500',
|
||||||
color === 'yellow' && 'bg-yellow-400 text-white hover:bg-yellow-500',
|
color === 'yellow' && 'bg-yellow-400 text-white hover:bg-yellow-500',
|
||||||
color === 'blue' && 'bg-blue-400 text-white hover:bg-blue-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',
|
color === 'gray' && 'bg-gray-200 text-gray-700 hover:bg-gray-300',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -18,10 +18,12 @@ export const useContract = (contractId: string) => {
|
||||||
return result.isLoading ? undefined : result.data
|
return result.isLoading ? undefined : result.data
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useContractWithPreload = (initial: Contract | null) => {
|
export const useContractWithPreload = (
|
||||||
const [contract, setContract] = useStateCheckEquality<Contract | null>(
|
initial: Contract | null | undefined
|
||||||
initial
|
) => {
|
||||||
)
|
const [contract, setContract] = useStateCheckEquality<
|
||||||
|
Contract | null | undefined
|
||||||
|
>(initial)
|
||||||
const contractId = initial?.id
|
const contractId = initial?.id
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
import { isEqual, sortBy } from 'lodash'
|
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { Fold } from 'common/fold'
|
import { Fold } from 'common/fold'
|
||||||
import { User } from 'common/user'
|
import { User } from 'common/user'
|
||||||
import {
|
import {
|
||||||
listAllFolds,
|
|
||||||
listenForFold,
|
listenForFold,
|
||||||
listenForFolds,
|
listenForFolds,
|
||||||
listenForFoldsWithTags,
|
listenForFoldsWithTags,
|
||||||
|
@ -80,38 +78,3 @@ export const useFollowedFoldIds = (user: User | null | undefined) => {
|
||||||
|
|
||||||
return followedFoldIds
|
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
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { collection, query, where } from 'firebase/firestore'
|
import { collection, query, where } from 'firebase/firestore'
|
||||||
import { Notification } from 'common/notification'
|
import { Notification } from 'common/notification'
|
||||||
import { db } from 'web/lib/firebase/init'
|
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) {
|
function getNotificationsQuery(userId: string, unseenOnly?: boolean) {
|
||||||
const notifsCollection = collection(db, `/users/${userId}/notifications`)
|
const notifsCollection = collection(db, `/users/${userId}/notifications`)
|
||||||
|
|
|
@ -39,6 +39,7 @@ import { FeedBet } from 'web/components/feed/feed-bets'
|
||||||
import { useIsIframe } from 'web/hooks/use-is-iframe'
|
import { useIsIframe } from 'web/hooks/use-is-iframe'
|
||||||
import ContractEmbedPage from '../embed/[username]/[contractSlug]'
|
import ContractEmbedPage from '../embed/[username]/[contractSlug]'
|
||||||
import { useBets } from 'web/hooks/use-bets'
|
import { useBets } from 'web/hooks/use-bets'
|
||||||
|
import { AlertBox } from 'web/components/alert-box'
|
||||||
|
|
||||||
export const getStaticProps = fromPropz(getStaticPropz)
|
export const getStaticProps = fromPropz(getStaticPropz)
|
||||||
export async function getStaticPropz(props: {
|
export async function getStaticPropz(props: {
|
||||||
|
@ -86,13 +87,13 @@ export default function ContractPage(props: {
|
||||||
slug: '',
|
slug: '',
|
||||||
}
|
}
|
||||||
|
|
||||||
const contract = useContractWithPreload(props.contract)
|
|
||||||
|
|
||||||
const inIframe = useIsIframe()
|
const inIframe = useIsIframe()
|
||||||
if (inIframe) {
|
if (inIframe) {
|
||||||
return <ContractEmbedPage {...props} />
|
return <ContractEmbedPage {...props} />
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { contract } = props
|
||||||
|
|
||||||
if (!contract) {
|
if (!contract) {
|
||||||
return <Custom404 />
|
return <Custom404 />
|
||||||
}
|
}
|
||||||
|
@ -103,7 +104,9 @@ export default function ContractPage(props: {
|
||||||
export function ContractPageContent(
|
export function ContractPageContent(
|
||||||
props: Parameters<typeof ContractPage>[0] & { contract: Contract }
|
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
|
const bets = useBets(contract.id) ?? props.bets
|
||||||
// Sort for now to see if bug is fixed.
|
// Sort for now to see if bug is fixed.
|
||||||
|
@ -187,6 +190,12 @@ export function ContractPageContent(
|
||||||
bets={bets}
|
bets={bets}
|
||||||
comments={comments ?? []}
|
comments={comments ?? []}
|
||||||
/>
|
/>
|
||||||
|
{isNumeric && (
|
||||||
|
<AlertBox
|
||||||
|
title="Warning"
|
||||||
|
text="Numeric markets were introduced as an experimental feature and are now deprecated."
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{outcomeType === 'FREE_RESPONSE' && (
|
{outcomeType === 'FREE_RESPONSE' && (
|
||||||
<>
|
<>
|
||||||
|
|
|
@ -37,7 +37,7 @@ export default function Activity() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Page assertUser="signed-in" suspend={!!contract}>
|
<Page suspend={!!contract}>
|
||||||
<Col className="mx-auto w-full max-w-[700px]">
|
<Col className="mx-auto w-full max-w-[700px]">
|
||||||
<CategorySelector category={category} setCategory={setCategory} />
|
<CategorySelector category={category} setCategory={setCategory} />
|
||||||
<Spacer h={1} />
|
<Spacer h={1} />
|
||||||
|
|
|
@ -16,7 +16,11 @@ export default function AddFundsPage() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page>
|
<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="items-center">
|
||||||
<Col className="h-full rounded bg-white p-4 py-8 sm:p-8 sm:shadow-md">
|
<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">
|
<div className="mb-6 text-gray-500">
|
||||||
Use Manifold Dollars to trade in your favorite markets. <br /> (Not
|
Purchase Manifold Dollars to trade in your favorite markets. <br />{' '}
|
||||||
redeemable for cash.)
|
(Not redeemable for cash.)
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mb-2 text-sm text-gray-500">Amount</div>
|
<div className="mb-2 text-sm text-gray-500">Amount</div>
|
||||||
|
@ -54,7 +58,7 @@ export default function AddFundsPage() {
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
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
|
Checkout
|
||||||
</button>
|
</button>
|
||||||
|
|
|
@ -19,6 +19,7 @@ export default async function handler(
|
||||||
listAllComments(contractId),
|
listAllComments(contractId),
|
||||||
])
|
])
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const bets = allBets.map(({ userId, ...bet }) => bet) as Exclude<
|
const bets = allBets.map(({ userId, ...bet }) => bet) as Exclude<
|
||||||
Bet,
|
Bet,
|
||||||
'userId'
|
'userId'
|
||||||
|
|
|
@ -24,6 +24,7 @@ export default async function handler(
|
||||||
listAllComments(contract.id),
|
listAllComments(contract.id),
|
||||||
])
|
])
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const bets = allBets.map(({ userId, ...bet }) => bet) as Exclude<
|
const bets = allBets.map(({ userId, ...bet }) => bet) as Exclude<
|
||||||
Bet,
|
Bet,
|
||||||
'userId'
|
'userId'
|
||||||
|
|
|
@ -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 { useState, useMemo } from 'react'
|
||||||
import { charities, Charity as CharityType } from 'common/charity'
|
import { charities, Charity as CharityType } from 'common/charity'
|
||||||
import { CharityCard } from 'web/components/charity/charity-card'
|
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 { SiteLink } from 'web/components/site-link'
|
||||||
import { Title } from 'web/components/title'
|
import { Title } from 'web/components/title'
|
||||||
import { getAllCharityTxns } from 'web/lib/firebase/txns'
|
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() {
|
export async function getStaticProps() {
|
||||||
const txns = await getAllCharityTxns()
|
const txns = await getAllCharityTxns()
|
||||||
|
@ -20,21 +30,55 @@ export async function getStaticProps() {
|
||||||
(charity) => (charity.tags?.includes('Featured') ? 0 : 1),
|
(charity) => (charity.tags?.includes('Featured') ? 0 : 1),
|
||||||
(charity) => -totals[charity.id],
|
(charity) => -totals[charity.id],
|
||||||
])
|
])
|
||||||
|
const matches = quadraticMatches(txns, totalRaised)
|
||||||
|
const numDonors = uniqBy(txns, (txn) => txn.fromId).length
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
totalRaised,
|
totalRaised,
|
||||||
charities: sortedCharities,
|
charities: sortedCharities,
|
||||||
|
matches,
|
||||||
|
txns,
|
||||||
|
numDonors,
|
||||||
},
|
},
|
||||||
revalidate: 60,
|
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: {
|
export default function Charity(props: {
|
||||||
totalRaised: number
|
totalRaised: number
|
||||||
charities: CharityType[]
|
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 [query, setQuery] = useState('')
|
||||||
const debouncedQuery = debounce(setQuery, 50)
|
const debouncedQuery = debounce(setQuery, 50)
|
||||||
|
@ -54,26 +98,51 @@ export default function Charity(props: {
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
<Col className="w-full rounded px-4 py-6 sm:px-8 xl:w-[125%]">
|
<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" />
|
<Title className="!mt-0" text="Manifold for Charity" />
|
||||||
<div className="mb-6 text-gray-500">
|
<span className="text-gray-600">
|
||||||
Donate your winnings to charity! Every {formatMoney(100)} you give
|
Through July 15, up to $25k of donations will be matched via{' '}
|
||||||
turns into $1 USD we send to your chosen charity.
|
<SiteLink href="https://wtfisqf.com/" className="font-bold">
|
||||||
<Spacer h={5} />
|
quadratic funding
|
||||||
Together we've donated over ${Math.floor(totalRaised / 100)} USD so
|
</SiteLink>
|
||||||
far!
|
, courtesy of{' '}
|
||||||
</div>
|
<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
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
onChange={(e) => debouncedQuery(e.target.value)}
|
onChange={(e) => debouncedQuery(e.target.value)}
|
||||||
placeholder="Search charities"
|
placeholder="Find a charity"
|
||||||
className="input input-bordered mb-6 w-full"
|
className="input input-bordered mb-6 w-full"
|
||||||
/>
|
/>
|
||||||
</Col>
|
</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">
|
<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) => (
|
{filterCharities.map((charity) => (
|
||||||
<CharityCard charity={charity} key={charity.name} />
|
<CharityCard
|
||||||
|
charity={charity}
|
||||||
|
key={charity.name}
|
||||||
|
match={matches[charity.id]}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
{filterCharities.length === 0 && (
|
{filterCharities.length === 0 && (
|
||||||
|
@ -82,32 +151,28 @@ export default function Charity(props: {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<iframe
|
<div className="mt-10 w-full rounded-xl bg-gradient-to-r from-pink-300 via-purple-300 to-indigo-400 p-5">
|
||||||
height="405"
|
<iframe
|
||||||
src="https://manifold.markets/embed/ManifoldMarkets/total-donations-for-manifold-for-go"
|
height="405"
|
||||||
title="Total donations for Manifold for Charity this May (in USD)"
|
src="https://manifold.markets/ManifoldMarkets/how-much-will-be-donated-through-ma"
|
||||||
frameBorder="0"
|
title="Total donations for Manifold for Charity this May (in USD)"
|
||||||
className="m-10 w-full rounded-xl bg-white p-10"
|
frameBorder="0"
|
||||||
></iframe>
|
className="w-full rounded-xl bg-white p-10"
|
||||||
|
></iframe>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="mt-10 text-gray-500">
|
<div className="mt-10 text-gray-500">
|
||||||
Don't see your favorite charity? Recommend it{' '}
|
<span className="font-semibold">Notes</span>
|
||||||
<SiteLink
|
|
||||||
href="https://manifold.markets/Sinclair/which-charities-should-manifold-add"
|
|
||||||
className="text-indigo-700"
|
|
||||||
>
|
|
||||||
here
|
|
||||||
</SiteLink>
|
|
||||||
!
|
|
||||||
<br />
|
<br />
|
||||||
|
- Don't see your favorite charity? Recommend it by emailing
|
||||||
|
charity@manifold.markets!
|
||||||
<br />
|
<br />
|
||||||
<span className="italic">
|
- Manifold is not affiliated with non-Featured charities; we're just
|
||||||
Note: Manifold is not affiliated with non-Featured charities; we're
|
fans of their work.
|
||||||
just fans of their work!
|
<br />
|
||||||
<br />
|
- As Manifold itself is a for-profit entity, your contributions will
|
||||||
As Manifold is a for-profit entity, your contributions will not be
|
not be tax deductible.
|
||||||
tax deductible.
|
<br />- Donations + matches are wired once each quarter.
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</Col>
|
</Col>
|
||||||
</Page>
|
</Page>
|
||||||
|
|
|
@ -56,8 +56,8 @@ export default function Create() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Allow user to create a new contract
|
// Allow user to create a new contract
|
||||||
export function NewContract(props: { question: string; tag?: string }) {
|
export function NewContract(props: { question: string }) {
|
||||||
const { question, tag } = props
|
const { question } = props
|
||||||
const creator = useUser()
|
const creator = useUser()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -74,7 +74,7 @@ export function NewContract(props: { question: string; tag?: string }) {
|
||||||
// const [tagText, setTagText] = useState<string>(tag ?? '')
|
// const [tagText, setTagText] = useState<string>(tag ?? '')
|
||||||
// const tags = parseWordsAsTags(tagText)
|
// const tags = parseWordsAsTags(tagText)
|
||||||
|
|
||||||
const [ante, setAnte] = useState(FIXED_ANTE)
|
const [ante, _setAnte] = useState(FIXED_ANTE)
|
||||||
|
|
||||||
const mustWaitForDailyFreeMarketStatus = useHasCreatedContractToday(creator)
|
const mustWaitForDailyFreeMarketStatus = useHasCreatedContractToday(creator)
|
||||||
|
|
||||||
|
@ -348,7 +348,7 @@ export function NewContract(props: { question: string; tag?: string }) {
|
||||||
className="btn btn-xs btn-primary"
|
className="btn btn-xs btn-primary"
|
||||||
onClick={() => (window.location.href = '/add-funds')}
|
onClick={() => (window.location.href = '/add-funds')}
|
||||||
>
|
>
|
||||||
Add funds
|
Get M$
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -27,10 +27,8 @@ import { EditFoldButton } from 'web/components/folds/edit-fold-button'
|
||||||
import Custom404 from '../../404'
|
import Custom404 from '../../404'
|
||||||
import { FollowFoldButton } from 'web/components/folds/follow-fold-button'
|
import { FollowFoldButton } from 'web/components/folds/follow-fold-button'
|
||||||
import { SEO } from 'web/components/SEO'
|
import { SEO } from 'web/components/SEO'
|
||||||
import { useTaggedContracts } from 'web/hooks/use-contracts'
|
|
||||||
import { Linkify } from 'web/components/linkify'
|
import { Linkify } from 'web/components/linkify'
|
||||||
import { fromPropz, usePropz } from 'web/hooks/use-propz'
|
import { fromPropz, usePropz } from 'web/hooks/use-propz'
|
||||||
import { filterDefined } from 'common/util/array'
|
|
||||||
import { findActiveContracts } from 'web/components/feed/find-active-contracts'
|
import { findActiveContracts } from 'web/components/feed/find-active-contracts'
|
||||||
import { Tabs } from 'web/components/layout/tabs'
|
import { Tabs } from 'web/components/layout/tabs'
|
||||||
|
|
||||||
|
@ -133,15 +131,6 @@ export default function FoldPage(props: {
|
||||||
const user = useUser()
|
const user = useUser()
|
||||||
const isCurator = user && fold && user.id === fold.curatorId
|
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]) {
|
if (fold === null || !foldSubpages.includes(page) || slugs[2]) {
|
||||||
return <Custom404 />
|
return <Custom404 />
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,7 +23,7 @@ const Home = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Page assertUser="signed-in" suspend={!!contract}>
|
<Page suspend={!!contract}>
|
||||||
<Col className="mx-auto w-full p-2">
|
<Col className="mx-auto w-full p-2">
|
||||||
<ContractSearch
|
<ContractSearch
|
||||||
querySortOptions={{
|
querySortOptions={{
|
||||||
|
|
|
@ -3,7 +3,7 @@ import Router from 'next/router'
|
||||||
|
|
||||||
import { Contract, getContractsBySlugs } from 'web/lib/firebase/contracts'
|
import { Contract, getContractsBySlugs } from 'web/lib/firebase/contracts'
|
||||||
import { Page } from 'web/components/page'
|
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 { Col } from 'web/components/layout/col'
|
||||||
import { useUser } from 'web/hooks/use-user'
|
import { useUser } from 'web/hooks/use-user'
|
||||||
import { ManifoldLogo } from 'web/components/nav/manifold-logo'
|
import { ManifoldLogo } from 'web/components/nav/manifold-logo'
|
||||||
|
@ -40,13 +40,13 @@ const Home = (props: { hotContracts: Contract[] }) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page assertUser="signed-out">
|
<Page>
|
||||||
<div className="px-4 pt-2 md:mt-0 lg:hidden">
|
<div className="px-4 pt-2 md:mt-0 lg:hidden">
|
||||||
<ManifoldLogo />
|
<ManifoldLogo />
|
||||||
</div>
|
</div>
|
||||||
<Col className="items-center">
|
<Col className="items-center">
|
||||||
<Col className="max-w-3xl">
|
<Col className="max-w-3xl">
|
||||||
<FeedPromo hotContracts={hotContracts ?? []} />
|
<LandingPagePanel hotContracts={hotContracts ?? []} />
|
||||||
{/* <p className="mt-6 text-gray-500">
|
{/* <p className="mt-6 text-gray-500">
|
||||||
View{' '}
|
View{' '}
|
||||||
<SiteLink href="/markets" className="font-bold text-gray-700">
|
<SiteLink href="/markets" className="font-bold text-gray-700">
|
||||||
|
|
|
@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -8,8 +8,8 @@ import { fromPropz, usePropz } from 'web/hooks/use-propz'
|
||||||
export const getStaticProps = fromPropz(getStaticPropz)
|
export const getStaticProps = fromPropz(getStaticPropz)
|
||||||
export async function getStaticPropz() {
|
export async function getStaticPropz() {
|
||||||
const [topTraders, topCreators] = await Promise.all([
|
const [topTraders, topCreators] = await Promise.all([
|
||||||
getTopTraders().catch((_) => {}),
|
getTopTraders().catch(() => {}),
|
||||||
getTopCreators().catch((_) => {}),
|
getTopCreators().catch(() => {}),
|
||||||
])
|
])
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -290,13 +290,3 @@ ${TEST_VALUE}
|
||||||
</Page>
|
</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()
|
|
||||||
}
|
|
||||||
|
|
|
@ -31,9 +31,7 @@ import { Linkify } from 'web/components/linkify'
|
||||||
import {
|
import {
|
||||||
BinaryOutcomeLabel,
|
BinaryOutcomeLabel,
|
||||||
CancelLabel,
|
CancelLabel,
|
||||||
FreeResponseOutcomeLabel,
|
|
||||||
MultiLabel,
|
MultiLabel,
|
||||||
OutcomeLabel,
|
|
||||||
ProbPercentLabel,
|
ProbPercentLabel,
|
||||||
} from 'web/components/outcome-label'
|
} from 'web/components/outcome-label'
|
||||||
import {
|
import {
|
||||||
|
@ -44,7 +42,7 @@ import {
|
||||||
import { getContractFromId } from 'web/lib/firebase/contracts'
|
import { getContractFromId } from 'web/lib/firebase/contracts'
|
||||||
import { CheckIcon, XIcon } from '@heroicons/react/outline'
|
import { CheckIcon, XIcon } from '@heroicons/react/outline'
|
||||||
import toast from 'react-hot-toast'
|
import toast from 'react-hot-toast'
|
||||||
import { formatMoney, formatPercent } from 'common/util/format'
|
import { formatMoney } from 'common/util/format'
|
||||||
|
|
||||||
export default function Notifications() {
|
export default function Notifications() {
|
||||||
const user = useUser()
|
const user = useUser()
|
||||||
|
@ -184,7 +182,7 @@ function NotificationGroupItem(props: {
|
||||||
className?: string
|
className?: string
|
||||||
}) {
|
}) {
|
||||||
const { notificationGroup, className } = props
|
const { notificationGroup, className } = props
|
||||||
const { sourceContractId, notifications, timePeriod } = notificationGroup
|
const { sourceContractId, notifications } = notificationGroup
|
||||||
const {
|
const {
|
||||||
sourceContractTitle,
|
sourceContractTitle,
|
||||||
sourceContractSlug,
|
sourceContractSlug,
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { zip, uniq, sumBy, concat, countBy, sortBy, sum } from 'lodash'
|
import { zip, uniq, sumBy, concat, countBy, sortBy, sum } from 'lodash'
|
||||||
import { IS_PRIVATE_MANIFOLD } from 'common/envs/constants'
|
|
||||||
import {
|
import {
|
||||||
DailyCountChart,
|
DailyCountChart,
|
||||||
DailyPercentChart,
|
DailyPercentChart,
|
||||||
|
@ -158,13 +157,11 @@ export async function getStaticPropz() {
|
||||||
bets?.map((b) => b.userId) ?? [],
|
bets?.map((b) => b.userId) ?? [],
|
||||||
comments?.map((c) => c.userId) ?? []
|
comments?.map((c) => c.userId) ?? []
|
||||||
)
|
)
|
||||||
const counts = Object.entries(countBy(userIds))
|
const counts = Object.values(countBy(userIds))
|
||||||
const topTenth = sortBy(counts, ([, count]) => count)
|
const sortedCounts = sortBy(counts, (count) => count).reverse()
|
||||||
.reverse()
|
if (sortedCounts.length === 0) return 0
|
||||||
// Take the top 10% of users, except for the top 2, to avoid outliers.
|
const tenthPercentile = sortedCounts[Math.floor(sortedCounts.length * 0.1)]
|
||||||
.slice(2, counts.length * 0.1)
|
return tenthPercentile
|
||||||
const topTenthTotal = sumBy(topTenth, ([_, count]) => count)
|
|
||||||
return topTenthTotal
|
|
||||||
})
|
})
|
||||||
const weeklyTopTenthActions = dailyTopTenthActions.map((_, i) => {
|
const weeklyTopTenthActions = dailyTopTenthActions.map((_, i) => {
|
||||||
const start = Math.max(0, i - 6)
|
const start = Math.max(0, i - 6)
|
||||||
|
@ -213,9 +210,11 @@ export async function getStaticPropz() {
|
||||||
weekOnWeekRetention,
|
weekOnWeekRetention,
|
||||||
weeklyActivationRate,
|
weeklyActivationRate,
|
||||||
monthlyRetention,
|
monthlyRetention,
|
||||||
dailyTopTenthActions,
|
topTenthActions: {
|
||||||
weeklyTopTenthActions,
|
daily: dailyTopTenthActions,
|
||||||
monthlyTopTenthActions,
|
weekly: weeklyTopTenthActions,
|
||||||
|
monthly: monthlyTopTenthActions,
|
||||||
|
},
|
||||||
manaBet: {
|
manaBet: {
|
||||||
daily: dailyManaBet,
|
daily: dailyManaBet,
|
||||||
weekly: weeklyManaBet,
|
weekly: weeklyManaBet,
|
||||||
|
@ -238,9 +237,11 @@ export default function Analytics(props: {
|
||||||
weekOnWeekRetention: number[]
|
weekOnWeekRetention: number[]
|
||||||
monthlyRetention: number[]
|
monthlyRetention: number[]
|
||||||
weeklyActivationRate: number[]
|
weeklyActivationRate: number[]
|
||||||
dailyTopTenthActions: number[]
|
topTenthActions: {
|
||||||
weeklyTopTenthActions: number[]
|
daily: number[]
|
||||||
monthlyTopTenthActions: number[]
|
weekly: number[]
|
||||||
|
monthly: number[]
|
||||||
|
}
|
||||||
manaBet: {
|
manaBet: {
|
||||||
daily: number[]
|
daily: number[]
|
||||||
weekly: number[]
|
weekly: number[]
|
||||||
|
@ -259,9 +260,11 @@ export default function Analytics(props: {
|
||||||
weekOnWeekRetention: [],
|
weekOnWeekRetention: [],
|
||||||
monthlyRetention: [],
|
monthlyRetention: [],
|
||||||
weeklyActivationRate: [],
|
weeklyActivationRate: [],
|
||||||
dailyTopTenthActions: [],
|
topTenthActions: {
|
||||||
weeklyTopTenthActions: [],
|
daily: [],
|
||||||
monthlyTopTenthActions: [],
|
weekly: [],
|
||||||
|
monthly: [],
|
||||||
|
},
|
||||||
manaBet: {
|
manaBet: {
|
||||||
daily: [],
|
daily: [],
|
||||||
weekly: [],
|
weekly: [],
|
||||||
|
@ -302,9 +305,11 @@ export function CustomAnalytics(props: {
|
||||||
weekOnWeekRetention: number[]
|
weekOnWeekRetention: number[]
|
||||||
monthlyRetention: number[]
|
monthlyRetention: number[]
|
||||||
weeklyActivationRate: number[]
|
weeklyActivationRate: number[]
|
||||||
dailyTopTenthActions: number[]
|
topTenthActions: {
|
||||||
weeklyTopTenthActions: number[]
|
daily: number[]
|
||||||
monthlyTopTenthActions: number[]
|
weekly: number[]
|
||||||
|
monthly: number[]
|
||||||
|
}
|
||||||
manaBet: {
|
manaBet: {
|
||||||
daily: number[]
|
daily: number[]
|
||||||
weekly: number[]
|
weekly: number[]
|
||||||
|
@ -322,9 +327,7 @@ export function CustomAnalytics(props: {
|
||||||
weekOnWeekRetention,
|
weekOnWeekRetention,
|
||||||
monthlyRetention,
|
monthlyRetention,
|
||||||
weeklyActivationRate,
|
weeklyActivationRate,
|
||||||
dailyTopTenthActions,
|
topTenthActions,
|
||||||
weeklyTopTenthActions,
|
|
||||||
monthlyTopTenthActions,
|
|
||||||
manaBet,
|
manaBet,
|
||||||
} = props
|
} = props
|
||||||
|
|
||||||
|
@ -526,10 +529,10 @@ export function CustomAnalytics(props: {
|
||||||
/>
|
/>
|
||||||
<Spacer h={8} />
|
<Spacer h={8} />
|
||||||
|
|
||||||
<Title text="Total actions by top tenth" />
|
<Title text="Action count of top tenth" />
|
||||||
<p className="text-gray-500">
|
<p className="text-gray-500">
|
||||||
From the top 10% of users, how many bets, comments, and markets did they
|
Number of actions (bets, comments, markets created) taken by the tenth
|
||||||
create? (Excluding top 2 users each day.)
|
percentile of top users.
|
||||||
</p>
|
</p>
|
||||||
<Tabs
|
<Tabs
|
||||||
defaultIndex={1}
|
defaultIndex={1}
|
||||||
|
@ -538,7 +541,7 @@ export function CustomAnalytics(props: {
|
||||||
title: 'Daily',
|
title: 'Daily',
|
||||||
content: (
|
content: (
|
||||||
<DailyCountChart
|
<DailyCountChart
|
||||||
dailyCounts={dailyTopTenthActions}
|
dailyCounts={topTenthActions.daily}
|
||||||
startDate={startDate}
|
startDate={startDate}
|
||||||
small
|
small
|
||||||
/>
|
/>
|
||||||
|
@ -548,7 +551,7 @@ export function CustomAnalytics(props: {
|
||||||
title: 'Weekly',
|
title: 'Weekly',
|
||||||
content: (
|
content: (
|
||||||
<DailyCountChart
|
<DailyCountChart
|
||||||
dailyCounts={weeklyTopTenthActions}
|
dailyCounts={topTenthActions.weekly}
|
||||||
startDate={startDate}
|
startDate={startDate}
|
||||||
small
|
small
|
||||||
/>
|
/>
|
||||||
|
@ -558,7 +561,7 @@ export function CustomAnalytics(props: {
|
||||||
title: 'Monthly',
|
title: 'Monthly',
|
||||||
content: (
|
content: (
|
||||||
<DailyCountChart
|
<DailyCountChart
|
||||||
dailyCounts={monthlyTopTenthActions}
|
dailyCounts={topTenthActions.monthly}
|
||||||
startDate={startDate}
|
startDate={startDate}
|
||||||
small
|
small
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -6,8 +6,8 @@ import { Title } from '../../components/title'
|
||||||
export default function TagPage() {
|
export default function TagPage() {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const { tag } = router.query as { tag: string }
|
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 (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
<Title text={`#${tag}`} />
|
<Title text={`#${tag}`} />
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 287 KiB After Width: | Height: | Size: 212 KiB |
Loading…
Reference in New Issue
Block a user