Merge branch 'main' into sell-panel
This commit is contained in:
commit
6c9145c5c6
|
@ -29,6 +29,7 @@ export type FullContract<
|
|||
|
||||
closeEmailsSent?: number
|
||||
|
||||
volume: number
|
||||
volume24Hours: number
|
||||
volume7Days: number
|
||||
|
||||
|
|
|
@ -34,6 +34,8 @@ export function getNewContract(
|
|||
? getBinaryCpmmProps(initialProb, ante) // getBinaryDpmProps(initialProb, ante)
|
||||
: getFreeAnswerProps(ante)
|
||||
|
||||
const volume = outcomeType === 'BINARY' ? 0 : ante
|
||||
|
||||
const contract: Contract = removeUndefinedProps({
|
||||
id,
|
||||
slug,
|
||||
|
@ -54,6 +56,7 @@ export function getNewContract(
|
|||
lastUpdatedTime: Date.now(),
|
||||
closeTime,
|
||||
|
||||
volume,
|
||||
volume24Hours: 0,
|
||||
volume7Days: 0,
|
||||
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
export function filterDefined<T>(array: (T | null | undefined)[]) {
|
||||
return array.filter((item) => item) as T[]
|
||||
return array.filter((item) => item !== null && item !== undefined) as T[]
|
||||
}
|
||||
|
|
|
@ -18,20 +18,10 @@ export function formatWithCommas(amount: number) {
|
|||
return formatter.format(amount).replace('$', '')
|
||||
}
|
||||
|
||||
const decimalPlaces = (x: number) => Math.ceil(-Math.log10(x)) - 2
|
||||
|
||||
export function formatPercent(decimalPercent: number) {
|
||||
const displayedFigs =
|
||||
(decimalPercent >= 0.02 && decimalPercent <= 0.98) ||
|
||||
decimalPercent <= 0 ||
|
||||
decimalPercent >= 1
|
||||
? 0
|
||||
: Math.max(
|
||||
decimalPlaces(decimalPercent),
|
||||
decimalPlaces(1 - decimalPercent)
|
||||
)
|
||||
|
||||
return (decimalPercent * 100).toFixed(displayedFigs) + '%'
|
||||
export function formatPercent(zeroToOne: number) {
|
||||
// Show 1 decimal place if <2% or >98%, giving more resolution on the tails
|
||||
const decimalPlaces = zeroToOne < 0.02 || zeroToOne > 0.98 ? 1 : 0
|
||||
return (zeroToOne * 100).toFixed(decimalPlaces) + '%'
|
||||
}
|
||||
|
||||
export function toCamelCase(words: string) {
|
||||
|
|
5
common/util/types.ts
Normal file
5
common/util/types.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
export type FirstArgument<T> = T extends (arg1: infer U, ...args: any[]) => any
|
||||
? U
|
||||
: any
|
||||
|
||||
export type Truthy<T> = Exclude<T, undefined | null | false | 0 | ''>
|
|
@ -57,7 +57,7 @@ export const createAnswer = functions.runWith({ minInstances: 1 }).https.onCall(
|
|||
message: 'Requires a free response contract',
|
||||
}
|
||||
|
||||
const { closeTime } = contract
|
||||
const { closeTime, volume } = contract
|
||||
if (closeTime && Date.now() > closeTime)
|
||||
return { status: 'error', message: 'Trading is closed' }
|
||||
|
||||
|
@ -121,6 +121,7 @@ export const createAnswer = functions.runWith({ minInstances: 1 }).https.onCall(
|
|||
totalShares: newTotalShares,
|
||||
totalBets: newTotalBets,
|
||||
answers: [...(contract.answers ?? []), answer],
|
||||
volume: volume + amount,
|
||||
})
|
||||
|
||||
if (!isFinite(newBalance)) {
|
||||
|
|
|
@ -49,7 +49,8 @@ export const placeBet = functions.runWith({ minInstances: 1 }).https.onCall(
|
|||
return { status: 'error', message: 'Invalid contract' }
|
||||
const contract = contractSnap.data() as Contract
|
||||
|
||||
const { closeTime, outcomeType, mechanism, collectedFees } = contract
|
||||
const { closeTime, outcomeType, mechanism, collectedFees, volume } =
|
||||
contract
|
||||
if (closeTime && Date.now() > closeTime)
|
||||
return { status: 'error', message: 'Trading is closed' }
|
||||
|
||||
|
@ -129,6 +130,7 @@ export const placeBet = functions.runWith({ minInstances: 1 }).https.onCall(
|
|||
totalBets: newTotalBets,
|
||||
totalLiquidity: newTotalLiquidity,
|
||||
collectedFees: addObjects<Fees>(fees ?? {}, collectedFees ?? {}),
|
||||
volume: volume + Math.abs(amount),
|
||||
})
|
||||
)
|
||||
|
||||
|
|
|
@ -35,7 +35,7 @@ export const sellBet = functions.runWith({ minInstances: 1 }).https.onCall(
|
|||
return { status: 'error', message: 'Invalid contract' }
|
||||
const contract = contractSnap.data() as Contract
|
||||
|
||||
const { closeTime, mechanism, collectedFees } = contract
|
||||
const { closeTime, mechanism, collectedFees, volume } = contract
|
||||
if (closeTime && Date.now() > closeTime)
|
||||
return { status: 'error', message: 'Trading is closed' }
|
||||
|
||||
|
@ -81,6 +81,7 @@ export const sellBet = functions.runWith({ minInstances: 1 }).https.onCall(
|
|||
totalShares: newTotalShares,
|
||||
totalBets: newTotalBets,
|
||||
collectedFees: addObjects<Fees>(fees ?? {}, collectedFees ?? {}),
|
||||
volume: volume + bet.amount,
|
||||
})
|
||||
)
|
||||
|
||||
|
|
|
@ -21,9 +21,11 @@ export function DailyCountChart(props: {
|
|||
}))
|
||||
const data = [{ id: 'Count', data: points, color: '#11b981' }]
|
||||
|
||||
const bottomAxisTicks = width && width < 600 ? 6 : undefined
|
||||
|
||||
return (
|
||||
<div
|
||||
className="w-full"
|
||||
className="w-full overflow-hidden"
|
||||
style={{ height: !small && (!width || width >= 800) ? 400 : 250 }}
|
||||
>
|
||||
<ResponsiveLine
|
||||
|
@ -33,10 +35,62 @@ export function DailyCountChart(props: {
|
|||
type: 'time',
|
||||
}}
|
||||
axisBottom={{
|
||||
tickValues: bottomAxisTicks,
|
||||
format: (date) => dayjs(date).format('MMM DD'),
|
||||
}}
|
||||
colors={{ datum: 'color' }}
|
||||
pointSize={width && width >= 800 ? 10 : 0}
|
||||
pointSize={0}
|
||||
pointBorderWidth={1}
|
||||
pointBorderColor="#fff"
|
||||
enableSlices="x"
|
||||
enableGridX={!!width && width >= 800}
|
||||
enableArea
|
||||
margin={{ top: 20, right: 28, bottom: 22, left: 40 }}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function DailyPercentChart(props: {
|
||||
startDate: number
|
||||
dailyPercent: number[]
|
||||
small?: boolean
|
||||
}) {
|
||||
const { dailyPercent, startDate, small } = props
|
||||
const { width } = useWindowSize()
|
||||
|
||||
const dates = dailyPercent.map((_, i) =>
|
||||
dayjs(startDate).add(i, 'day').toDate()
|
||||
)
|
||||
|
||||
const points = _.zip(dates, dailyPercent).map(([date, betCount]) => ({
|
||||
x: date,
|
||||
y: betCount,
|
||||
}))
|
||||
const data = [{ id: 'Percent', data: points, color: '#11b981' }]
|
||||
|
||||
const bottomAxisTicks = width && width < 600 ? 6 : undefined
|
||||
|
||||
return (
|
||||
<div
|
||||
className="w-full overflow-hidden"
|
||||
style={{ height: !small && (!width || width >= 800) ? 400 : 250 }}
|
||||
>
|
||||
<ResponsiveLine
|
||||
data={data}
|
||||
yScale={{ type: 'linear', stacked: false }}
|
||||
xScale={{
|
||||
type: 'time',
|
||||
}}
|
||||
axisLeft={{
|
||||
format: (value) => `${value}%`,
|
||||
}}
|
||||
axisBottom={{
|
||||
tickValues: bottomAxisTicks,
|
||||
format: (date) => dayjs(date).format('MMM DD'),
|
||||
}}
|
||||
colors={{ datum: 'color' }}
|
||||
pointSize={0}
|
||||
pointBorderWidth={1}
|
||||
pointBorderColor="#fff"
|
||||
enableSlices="x"
|
||||
|
|
|
@ -63,7 +63,7 @@ export function AnswerBetPanel(props: {
|
|||
|
||||
if (result?.status === 'success') {
|
||||
setIsSubmitting(false)
|
||||
closePanel()
|
||||
setBetAmount(undefined)
|
||||
} else {
|
||||
setError(result?.error || 'Error placing bet')
|
||||
setIsSubmitting(false)
|
||||
|
@ -134,9 +134,11 @@ export function AnswerBetPanel(props: {
|
|||
</Row>
|
||||
</Row>
|
||||
|
||||
<Row className="items-start justify-between gap-2 text-sm">
|
||||
<Row className="items-center justify-between gap-2 text-sm">
|
||||
<Row className="flex-nowrap items-center gap-2 whitespace-nowrap text-gray-500">
|
||||
<div>Payout if chosen</div>
|
||||
<div>
|
||||
Estimated <br /> payout if chosen
|
||||
</div>
|
||||
<InfoTooltip
|
||||
text={`Current payout for ${formatWithCommas(
|
||||
shares
|
||||
|
|
|
@ -14,8 +14,9 @@ const NUM_LINES = 6
|
|||
export function AnswersGraph(props: {
|
||||
contract: FullContract<DPM, FreeResponse>
|
||||
bets: Bet[]
|
||||
height?: number
|
||||
}) {
|
||||
const { contract } = props
|
||||
const { contract, height } = props
|
||||
const { createdTime, resolutionTime, closeTime, answers } = contract
|
||||
|
||||
const bets = useBets(contract.id) ?? props.bets
|
||||
|
@ -86,7 +87,7 @@ export function AnswersGraph(props: {
|
|||
return (
|
||||
<div
|
||||
className="w-full overflow-hidden"
|
||||
style={{ height: !width || width >= 800 ? 350 : 225 }}
|
||||
style={{ height: height ?? (!width || width >= 800 ? 350 : 225) }}
|
||||
>
|
||||
<ResponsiveLine
|
||||
data={data}
|
||||
|
@ -137,12 +138,14 @@ const computeProbsByOutcome = (
|
|||
bets: Bet[],
|
||||
contract: FullContract<DPM, FreeResponse>
|
||||
) => {
|
||||
const { totalBets } = contract
|
||||
|
||||
const betsByOutcome = _.groupBy(bets, (bet) => bet.outcome)
|
||||
const outcomes = Object.keys(betsByOutcome).filter((outcome) => {
|
||||
const maxProb = Math.max(
|
||||
...betsByOutcome[outcome].map((bet) => bet.probAfter)
|
||||
)
|
||||
return outcome !== '0' && maxProb > 0.05
|
||||
return outcome !== '0' && maxProb > 0.02 && totalBets[outcome] > 0.000000001
|
||||
})
|
||||
|
||||
const trackedOutcomes = _.sortBy(
|
||||
|
|
|
@ -81,7 +81,7 @@ export function CreateAnswerPanel(props: {
|
|||
<Textarea
|
||||
value={text}
|
||||
onChange={(e) => setText(e.target.value)}
|
||||
className="textarea textarea-bordered w-full"
|
||||
className="textarea textarea-bordered w-full resize-none"
|
||||
placeholder="Type your answer..."
|
||||
rows={1}
|
||||
maxLength={10000}
|
||||
|
@ -117,9 +117,11 @@ export function CreateAnswerPanel(props: {
|
|||
</Row>
|
||||
</Row>
|
||||
|
||||
<Row className="justify-between gap-2 text-sm">
|
||||
<Row className="items-center justify-between gap-4 text-sm">
|
||||
<Row className="flex-nowrap items-center gap-2 whitespace-nowrap text-gray-500">
|
||||
<div>Payout if chosen</div>
|
||||
<div>
|
||||
Estimated <br /> payout if chosen
|
||||
</div>
|
||||
<InfoTooltip
|
||||
text={`Current payout for ${formatWithCommas(
|
||||
shares
|
||||
|
|
|
@ -223,7 +223,7 @@ function BuyPanel(props: {
|
|||
focusAmountInput()
|
||||
}, [focusAmountInput])
|
||||
|
||||
const tooltip =
|
||||
const dpmTooltip =
|
||||
contract.mechanism === 'dpm-2'
|
||||
? `Current payout for ${formatWithCommas(shares)} / ${formatWithCommas(
|
||||
shares +
|
||||
|
@ -231,7 +231,7 @@ function BuyPanel(props: {
|
|||
(contract.phantomShares
|
||||
? contract.phantomShares[betChoice ?? 'YES']
|
||||
: 0)
|
||||
)} ${betChoice} shares`
|
||||
)} ${betChoice ?? 'YES'} shares`
|
||||
: undefined
|
||||
return (
|
||||
<>
|
||||
|
@ -263,13 +263,22 @@ function BuyPanel(props: {
|
|||
</Row>
|
||||
</Row>
|
||||
|
||||
<Row className="items-start justify-between gap-2 text-sm">
|
||||
<Row className="items-center justify-between gap-2 text-sm">
|
||||
<Row className="flex-nowrap items-center gap-2 whitespace-nowrap text-gray-500">
|
||||
<div>
|
||||
Payout if <OutcomeLabel outcome={betChoice ?? 'YES'} />
|
||||
{contract.mechanism === 'dpm-2' ? (
|
||||
<>
|
||||
Estimated
|
||||
<br /> payout if <OutcomeLabel outcome={betChoice ?? 'YES'} />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
Payout if <OutcomeLabel outcome={betChoice ?? 'YES'} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{tooltip && <InfoTooltip text={tooltip} />}
|
||||
{dpmTooltip && <InfoTooltip text={dpmTooltip} />}
|
||||
</Row>
|
||||
<Row className="flex-wrap items-end justify-end gap-2">
|
||||
<span className="whitespace-nowrap">
|
||||
|
|
|
@ -37,7 +37,7 @@ import {
|
|||
resolvedPayout,
|
||||
} from '../../common/calculate'
|
||||
|
||||
type BetSort = 'newest' | 'profit' | 'settled' | 'value'
|
||||
type BetSort = 'newest' | 'profit' | 'resolutionTime' | 'value' | 'closeTime'
|
||||
type BetFilter = 'open' | 'closed' | 'resolved' | 'all'
|
||||
|
||||
export function BetsList(props: { user: User }) {
|
||||
|
@ -91,7 +91,7 @@ export function BetsList(props: { user: User }) {
|
|||
const contractsInvestment = _.mapValues(contractBets, (bets) => {
|
||||
return _.sumBy(bets, (bet) => {
|
||||
if (bet.isSold || bet.sale) return 0
|
||||
return bet.amount
|
||||
return bet.amount - (bet.loanAmount ?? 0)
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -108,7 +108,8 @@ export function BetsList(props: { user: User }) {
|
|||
value: (c) => contractsCurrentValue[c.id],
|
||||
newest: (c) =>
|
||||
Math.max(...contractBets[c.id].map((bet) => bet.createdTime)),
|
||||
settled: (c) => c.resolutionTime ?? 0,
|
||||
resolutionTime: (c) => -(c.resolutionTime ?? c.closeTime ?? Infinity),
|
||||
closeTime: (c) => -(c.closeTime ?? Infinity),
|
||||
}
|
||||
const displayedContracts = _.sortBy(contracts, SORTS[sort])
|
||||
.reverse()
|
||||
|
@ -173,7 +174,8 @@ export function BetsList(props: { user: User }) {
|
|||
<option value="value">By value</option>
|
||||
<option value="profit">By profit</option>
|
||||
<option value="newest">Most recent</option>
|
||||
<option value="settled">By resolution time</option>
|
||||
<option value="closeTime">Closing soonest</option>
|
||||
<option value="resolutionTime">Resolved soonest</option>
|
||||
</select>
|
||||
</Row>
|
||||
</Col>
|
||||
|
@ -449,7 +451,7 @@ export function ContractBetsTable(props: {
|
|||
<div className={clsx('overflow-x-auto', className)}>
|
||||
{amountRedeemed > 0 && (
|
||||
<>
|
||||
<div className="text-gray-500 text-sm pl-2">
|
||||
<div className="pl-2 text-sm text-gray-500">
|
||||
{amountRedeemed} YES shares and {amountRedeemed} NO shares
|
||||
automatically redeemed for {formatMoney(amountRedeemed)}.
|
||||
</div>
|
||||
|
@ -459,7 +461,7 @@ export function ContractBetsTable(props: {
|
|||
|
||||
{!isResolved && amountLoaned > 0 && (
|
||||
<>
|
||||
<div className="text-gray-500 text-sm pl-2">
|
||||
<div className="pl-2 text-sm text-gray-500">
|
||||
You currently have a loan of {formatMoney(amountLoaned)}.
|
||||
</div>
|
||||
<Spacer h={4} />
|
||||
|
@ -505,7 +507,6 @@ function BetRow(props: { bet: Bet; contract: Contract; saleBet?: Bet }) {
|
|||
shares,
|
||||
isSold,
|
||||
isAnte,
|
||||
loanAmount,
|
||||
} = bet
|
||||
|
||||
const { isResolved, closeTime, mechanism } = contract
|
||||
|
|
|
@ -21,6 +21,7 @@ import { Spacer } from './layout/spacer'
|
|||
import { useState } from 'react'
|
||||
import { TweetButton } from './tweet-button'
|
||||
import { getProbability } from '../../common/calculate'
|
||||
import { ShareEmbedButton } from './share-embed-button'
|
||||
|
||||
export function ContractCard(props: {
|
||||
contract: Contract
|
||||
|
@ -130,7 +131,7 @@ function AbbrContractDetails(props: {
|
|||
}) {
|
||||
const { contract, showHotVolume, showCloseTime } = props
|
||||
const { volume24Hours, creatorName, creatorUsername, closeTime } = contract
|
||||
const { liquidityLabel } = contractMetrics(contract)
|
||||
const { volumeLabel } = contractMetrics(contract)
|
||||
|
||||
return (
|
||||
<Col className={clsx('gap-2 text-sm text-gray-500')}>
|
||||
|
@ -161,7 +162,7 @@ function AbbrContractDetails(props: {
|
|||
) : (
|
||||
<Row className="gap-1">
|
||||
{/* <DatabaseIcon className="h-5 w-5" /> */}
|
||||
{liquidityLabel}
|
||||
{volumeLabel}
|
||||
</Row>
|
||||
)}
|
||||
</Row>
|
||||
|
@ -172,17 +173,17 @@ function AbbrContractDetails(props: {
|
|||
export function ContractDetails(props: {
|
||||
contract: Contract
|
||||
isCreator?: boolean
|
||||
hideShareButtons?: boolean
|
||||
}) {
|
||||
const { contract, isCreator } = props
|
||||
const { contract, isCreator, hideShareButtons } = props
|
||||
const { closeTime, creatorName, creatorUsername } = contract
|
||||
const { liquidityLabel, createdDate, resolvedDate } =
|
||||
contractMetrics(contract)
|
||||
const { volumeLabel, createdDate, resolvedDate } = contractMetrics(contract)
|
||||
|
||||
const tweetText = getTweetText(contract, !!isCreator)
|
||||
|
||||
return (
|
||||
<Col className="gap-2 text-sm text-gray-500 sm:flex-row sm:flex-wrap">
|
||||
<Row className="flex-wrap items-center gap-x-4 gap-y-2">
|
||||
<Row className="flex-wrap items-center gap-x-4 gap-y-3">
|
||||
<Row className="items-center gap-2">
|
||||
<Avatar
|
||||
username={creatorUsername}
|
||||
|
@ -230,10 +231,15 @@ export function ContractDetails(props: {
|
|||
<Row className="items-center gap-1">
|
||||
<DatabaseIcon className="h-5 w-5" />
|
||||
|
||||
<div className="whitespace-nowrap">{liquidityLabel}</div>
|
||||
<div className="whitespace-nowrap">{volumeLabel}</div>
|
||||
</Row>
|
||||
|
||||
<TweetButton className="self-end" tweetText={tweetText} />
|
||||
{!hideShareButtons && (
|
||||
<>
|
||||
<TweetButton className="self-end" tweetText={tweetText} />
|
||||
<ShareEmbedButton contract={contract} />
|
||||
</>
|
||||
)}
|
||||
</Row>
|
||||
</Col>
|
||||
)
|
||||
|
@ -242,8 +248,7 @@ export function ContractDetails(props: {
|
|||
// String version of the above, to send to the OpenGraph image generator
|
||||
export function contractTextDetails(contract: Contract) {
|
||||
const { closeTime, tags } = contract
|
||||
const { createdDate, resolvedDate, liquidityLabel } =
|
||||
contractMetrics(contract)
|
||||
const { createdDate, resolvedDate, volumeLabel } = contractMetrics(contract)
|
||||
|
||||
const hashtags = tags.map((tag) => `#${tag}`)
|
||||
|
||||
|
@ -254,7 +259,7 @@ export function contractTextDetails(contract: Contract) {
|
|||
closeTime
|
||||
).format('MMM D, h:mma')}`
|
||||
: '') +
|
||||
` • ${liquidityLabel}` +
|
||||
` • ${volumeLabel}` +
|
||||
(hashtags.length > 0 ? ` • ${hashtags.join(' ')}` : '')
|
||||
)
|
||||
}
|
||||
|
|
|
@ -34,43 +34,33 @@ export const ContractOverview = (props: {
|
|||
|
||||
return (
|
||||
<Col className={clsx('mb-6', className)}>
|
||||
<Row className="justify-between gap-4 px-2">
|
||||
<Col className="gap-4">
|
||||
<Col className="gap-4 px-2">
|
||||
<Row className="justify-between gap-4">
|
||||
<div className="text-2xl text-indigo-700 md:text-3xl">
|
||||
<Linkify text={question} />
|
||||
</div>
|
||||
|
||||
<Row className="items-center justify-between gap-4">
|
||||
{(isBinary || resolution) && (
|
||||
<ResolutionOrChance
|
||||
className="md:hidden"
|
||||
contract={contract}
|
||||
large
|
||||
/>
|
||||
)}
|
||||
|
||||
{isBinary && tradingAllowed(contract) && (
|
||||
<BetRow
|
||||
contract={contract}
|
||||
className="md:hidden"
|
||||
labelClassName="hidden"
|
||||
/>
|
||||
)}
|
||||
</Row>
|
||||
|
||||
<ContractDetails contract={contract} isCreator={isCreator} />
|
||||
</Col>
|
||||
|
||||
{(isBinary || resolution) && (
|
||||
<Col className="hidden items-end justify-between md:flex">
|
||||
{(isBinary || resolution) && (
|
||||
<ResolutionOrChance
|
||||
className="items-end"
|
||||
className="hidden md:flex items-end"
|
||||
contract={contract}
|
||||
large
|
||||
/>
|
||||
</Col>
|
||||
)}
|
||||
</Row>
|
||||
)}
|
||||
</Row>
|
||||
|
||||
<Row className="md:hidden items-center justify-between gap-4">
|
||||
{(isBinary || resolution) && (
|
||||
<ResolutionOrChance contract={contract} />
|
||||
)}
|
||||
|
||||
{isBinary && tradingAllowed(contract) && (
|
||||
<BetRow contract={contract} labelClassName="hidden" />
|
||||
)}
|
||||
</Row>
|
||||
|
||||
<ContractDetails contract={contract} isCreator={isCreator} />
|
||||
</Col>
|
||||
|
||||
<Spacer h={4} />
|
||||
|
||||
|
|
|
@ -10,8 +10,9 @@ import { useWindowSize } from '../hooks/use-window-size'
|
|||
export function ContractProbGraph(props: {
|
||||
contract: FullContract<DPM | CPMM, Binary>
|
||||
bets: Bet[]
|
||||
height?: number
|
||||
}) {
|
||||
const { contract } = props
|
||||
const { contract, height } = props
|
||||
const { resolutionTime, closeTime } = contract
|
||||
|
||||
const bets = useBetsWithoutAntes(contract, props.bets).filter(
|
||||
|
@ -63,7 +64,7 @@ export function ContractProbGraph(props: {
|
|||
return (
|
||||
<div
|
||||
className="w-full overflow-hidden"
|
||||
style={{ height: !width || width >= 800 ? 400 : 250 }}
|
||||
style={{ height: height ?? (!width || width >= 800 ? 400 : 250) }}
|
||||
>
|
||||
<ResponsiveLine
|
||||
data={data}
|
||||
|
|
|
@ -7,7 +7,6 @@ import {
|
|||
contractMetrics,
|
||||
Contract,
|
||||
listContracts,
|
||||
getBinaryProbPercent,
|
||||
getBinaryProb,
|
||||
} from '../lib/firebase/contracts'
|
||||
import { User } from '../lib/firebase/users'
|
||||
|
@ -16,7 +15,6 @@ import { SiteLink } from './site-link'
|
|||
import { ContractCard } from './contract-card'
|
||||
import { Sort, useQueryAndSortParams } from '../hooks/use-sort-and-query-params'
|
||||
import { Answer } from '../../common/answer'
|
||||
import { getProbability } from '../../common/calculate'
|
||||
|
||||
export function ContractsGrid(props: {
|
||||
contracts: Contract[]
|
||||
|
@ -249,9 +247,7 @@ export function SearchableGrid(props: {
|
|||
({ closeTime }) => closeTime && closeTime > Date.now() !== hideClosed
|
||||
)
|
||||
} else if (sort === 'most-traded') {
|
||||
matches.sort(
|
||||
(a, b) => contractMetrics(b).truePool - contractMetrics(a).truePool
|
||||
)
|
||||
matches.sort((a, b) => b.volume - a.volume)
|
||||
} else if (sort === '24-hour-vol') {
|
||||
// Use lodash for stable sort, so previous sort breaks all ties.
|
||||
matches = _.sortBy(matches, ({ volume7Days }) => -1 * volume7Days)
|
||||
|
|
|
@ -47,6 +47,7 @@ import { BuyButton } from '../yes-no-selector'
|
|||
import { getDpmOutcomeProbability } from '../../../common/calculate-dpm'
|
||||
import { AnswerBetPanel } from '../answers/answer-bet-panel'
|
||||
import { useSaveSeenContract } from '../../hooks/use-seen-contracts'
|
||||
import { User } from '../../../common/user'
|
||||
|
||||
export function FeedItems(props: {
|
||||
contract: Contract
|
||||
|
@ -109,7 +110,7 @@ function FeedItem(props: { item: ActivityItem }) {
|
|||
}
|
||||
}
|
||||
|
||||
function FeedComment(props: {
|
||||
export function FeedComment(props: {
|
||||
contract: Contract
|
||||
comment: Comment
|
||||
bet: Bet
|
||||
|
@ -171,13 +172,14 @@ function RelativeTimestamp(props: { time: number }) {
|
|||
)
|
||||
}
|
||||
|
||||
function FeedBet(props: {
|
||||
export function FeedBet(props: {
|
||||
contract: Contract
|
||||
bet: Bet
|
||||
hideOutcome: boolean
|
||||
smallAvatar: boolean
|
||||
bettor?: User // If set: reveal bettor identity
|
||||
}) {
|
||||
const { contract, bet, hideOutcome, smallAvatar } = props
|
||||
const { contract, bet, hideOutcome, smallAvatar, bettor } = props
|
||||
const { id, amount, outcome, createdTime, userId } = bet
|
||||
const user = useUser()
|
||||
const isSelf = user?.id === userId
|
||||
|
@ -204,6 +206,13 @@ function FeedBet(props: {
|
|||
avatarUrl={user.avatarUrl}
|
||||
username={user.username}
|
||||
/>
|
||||
) : bettor ? (
|
||||
<Avatar
|
||||
className={clsx(smallAvatar && 'ml-1')}
|
||||
size={smallAvatar ? 'sm' : undefined}
|
||||
avatarUrl={bettor.avatarUrl}
|
||||
username={bettor.username}
|
||||
/>
|
||||
) : (
|
||||
<div className="relative px-1">
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-gray-200">
|
||||
|
@ -212,9 +221,10 @@ function FeedBet(props: {
|
|||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className={'min-w-0 flex-1 pb-1.5'}>
|
||||
<div className={'min-w-0 flex-1 py-1.5'}>
|
||||
<div className="text-sm text-gray-500">
|
||||
<span>{isSelf ? 'You' : 'A trader'}</span> {bought} {money}
|
||||
<span>{isSelf ? 'You' : bettor ? bettor.name : 'A trader'}</span>{' '}
|
||||
{bought} {money}
|
||||
{!hideOutcome && (
|
||||
<>
|
||||
{' '}
|
||||
|
@ -227,7 +237,7 @@ function FeedBet(props: {
|
|||
<Textarea
|
||||
value={comment}
|
||||
onChange={(e) => setComment(e.target.value)}
|
||||
className="textarea textarea-bordered w-full"
|
||||
className="textarea textarea-bordered w-full resize-none"
|
||||
placeholder="Add a comment..."
|
||||
rows={3}
|
||||
maxLength={MAX_COMMENT_LENGTH}
|
||||
|
@ -268,7 +278,7 @@ function EditContract(props: {
|
|||
return editing ? (
|
||||
<div className="mt-4">
|
||||
<Textarea
|
||||
className="textarea textarea-bordered mb-1 h-24 w-full"
|
||||
className="textarea textarea-bordered mb-1 h-24 w-full resize-none"
|
||||
rows={3}
|
||||
value={text}
|
||||
onChange={(e) => setText(e.target.value || '')}
|
||||
|
@ -407,7 +417,7 @@ export function FeedQuestion(props: {
|
|||
const { contract, showDescription } = props
|
||||
const { creatorName, creatorUsername, question, resolution, outcomeType } =
|
||||
contract
|
||||
const { liquidityLabel } = contractMetrics(contract)
|
||||
const { volumeLabel } = contractMetrics(contract)
|
||||
const isBinary = outcomeType === 'BINARY'
|
||||
|
||||
const closeMessage =
|
||||
|
@ -435,7 +445,7 @@ export function FeedQuestion(props: {
|
|||
asked
|
||||
{/* Currently hidden on mobile; ideally we'd fit this in somewhere. */}
|
||||
<span className="float-right hidden text-gray-400 sm:inline">
|
||||
{liquidityLabel}
|
||||
{volumeLabel}
|
||||
{closeMessage}
|
||||
</span>
|
||||
</div>
|
||||
|
@ -697,7 +707,7 @@ function FeedAnswerGroup(props: {
|
|||
</div>
|
||||
|
||||
<Row className="align-items justify-between gap-4">
|
||||
<span className="text-lg">
|
||||
<span className="whitespace-pre-line text-lg">
|
||||
<Linkify text={text} />
|
||||
</span>
|
||||
|
||||
|
|
|
@ -1,15 +1,16 @@
|
|||
import clsx from 'clsx'
|
||||
import { CSSProperties } from 'react'
|
||||
import { CSSProperties, Ref } from 'react'
|
||||
|
||||
export function Col(props: {
|
||||
children?: any
|
||||
className?: string
|
||||
style?: CSSProperties
|
||||
ref?: Ref<HTMLDivElement>
|
||||
}) {
|
||||
const { children, className, style } = props
|
||||
const { children, className, style, ref } = props
|
||||
|
||||
return (
|
||||
<div className={clsx(className, 'flex flex-col')} style={style}>
|
||||
<div className={clsx(className, 'flex flex-col')} style={style} ref={ref}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
|
|
|
@ -14,6 +14,7 @@ export function Leaderboard(props: {
|
|||
}[]
|
||||
className?: string
|
||||
}) {
|
||||
// TODO: Ideally, highlight your own entry on the leaderboard
|
||||
const { title, users, columns, className } = props
|
||||
return (
|
||||
<div className={clsx('w-full px-1', className)}>
|
||||
|
|
56
web/components/share-embed-button.tsx
Normal file
56
web/components/share-embed-button.tsx
Normal file
|
@ -0,0 +1,56 @@
|
|||
import { Fragment } from 'react'
|
||||
import { CodeIcon } from '@heroicons/react/outline'
|
||||
import { Menu, Transition } from '@headlessui/react'
|
||||
import { Contract } from '../../common/contract'
|
||||
import { contractPath } from '../lib/firebase/contracts'
|
||||
import { DOMAIN } from '../../common/envs/constants'
|
||||
import { copyToClipboard } from '../lib/util/copy'
|
||||
|
||||
export function ShareEmbedButton(props: { contract: Contract }) {
|
||||
const { contract } = props
|
||||
|
||||
const copyEmbed = () => {
|
||||
const title = contract.question
|
||||
const src = `https://${DOMAIN}/embed${contractPath(contract)}`
|
||||
|
||||
const embedCode = `<iframe width="560" height="405" src="${src}" title="${title}" frameborder="0"></iframe>`
|
||||
|
||||
copyToClipboard(embedCode)
|
||||
}
|
||||
|
||||
return (
|
||||
<Menu
|
||||
as="div"
|
||||
className="relative z-10 flex-shrink-0"
|
||||
onMouseUp={copyEmbed}
|
||||
>
|
||||
<Menu.Button
|
||||
className="btn btn-xs normal-case"
|
||||
style={{
|
||||
backgroundColor: 'white',
|
||||
border: '2px solid #9ca3af',
|
||||
color: '#9ca3af', // text-gray-400
|
||||
}}
|
||||
>
|
||||
<CodeIcon className="mr-1.5 h-4 w-4" aria-hidden="true" />
|
||||
Embed
|
||||
</Menu.Button>
|
||||
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Menu.Items className="origin-top-center absolute left-0 mt-2 w-40 rounded-md bg-white py-1 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
|
||||
<Menu.Item>
|
||||
<div className="px-2 py-1">Embed code copied!</div>
|
||||
</Menu.Item>
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</Menu>
|
||||
)
|
||||
}
|
|
@ -3,7 +3,7 @@ import Link from 'next/link'
|
|||
|
||||
export const SiteLink = (props: {
|
||||
href: string
|
||||
children: any
|
||||
children?: any
|
||||
className?: string
|
||||
}) => {
|
||||
const { href, children, className } = props
|
||||
|
|
|
@ -5,10 +5,7 @@ export function TweetButton(props: { className?: string; tweetText?: string }) {
|
|||
|
||||
return (
|
||||
<a
|
||||
className={clsx(
|
||||
'btn btn-xs flex flex-row flex-nowrap border-none normal-case',
|
||||
className
|
||||
)}
|
||||
className={clsx('btn btn-xs flex-nowrap normal-case', className)}
|
||||
style={{
|
||||
backgroundColor: 'white',
|
||||
border: '2px solid #1da1f2',
|
||||
|
|
|
@ -74,7 +74,10 @@ export const useFilterYourContracts = (
|
|||
if (yourBetContracts && followedFoldIds) {
|
||||
// Show default contracts if no folds are followed.
|
||||
if (followedFoldIds.length === 0)
|
||||
yourContracts = contracts.filter(includedWithDefaultFeed)
|
||||
yourContracts = contracts.filter(
|
||||
(contract) =>
|
||||
includedWithDefaultFeed(contract) || yourBetContracts.has(contract.id)
|
||||
)
|
||||
else
|
||||
yourContracts = contracts.filter(
|
||||
(contract) =>
|
||||
|
|
63
web/hooks/use-measure-size.ts
Normal file
63
web/hooks/use-measure-size.ts
Normal file
|
@ -0,0 +1,63 @@
|
|||
import _ from 'lodash'
|
||||
import { RefObject, useMemo, useLayoutEffect, useRef, useState } from 'react'
|
||||
|
||||
type elem_size =
|
||||
| { width: number; height: number }
|
||||
| { width: undefined; height: undefined }
|
||||
|
||||
const getSize = (elem: HTMLElement | null) =>
|
||||
elem
|
||||
? { width: elem.offsetWidth, height: elem.offsetHeight }
|
||||
: { width: undefined, height: undefined }
|
||||
|
||||
export function useListenElemSize<T extends HTMLElement>(
|
||||
elemRef: RefObject<T | null>,
|
||||
callback: (size: elem_size) => void,
|
||||
debounceMs: number | undefined = undefined
|
||||
) {
|
||||
const handleResize = useMemo(() => {
|
||||
let updateSize = () => {
|
||||
if (elemRef.current) callback(getSize(elemRef.current))
|
||||
}
|
||||
|
||||
return debounceMs
|
||||
? _.debounce(updateSize, debounceMs, { leading: false, trailing: true })
|
||||
: updateSize
|
||||
}, [callback, elemRef, debounceMs])
|
||||
|
||||
let elem = elemRef.current
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!elemRef.current) return
|
||||
|
||||
const resizeObserver = new ResizeObserver(handleResize)
|
||||
resizeObserver.observe(elemRef.current)
|
||||
|
||||
return () => resizeObserver.disconnect()
|
||||
}, [elemRef, elem, handleResize])
|
||||
}
|
||||
|
||||
export function useMeasureSize(debounceMs: number | undefined = undefined) {
|
||||
const elemRef = useRef<HTMLElement | null>(null)
|
||||
const [size, setSize] = useState(() => getSize(null))
|
||||
const sizeRef = useRef<elem_size>(size)
|
||||
|
||||
const setSizeIfDifferent = (newSize: typeof size) => {
|
||||
if (newSize?.height !== size?.height || newSize?.width !== size?.width) {
|
||||
sizeRef.current = newSize
|
||||
setSize(newSize)
|
||||
}
|
||||
}
|
||||
|
||||
useListenElemSize(elemRef, setSizeIfDifferent, debounceMs)
|
||||
|
||||
const setElem = (elem: HTMLElement | null) => {
|
||||
elemRef.current = elem
|
||||
|
||||
if (elem) {
|
||||
setSizeIfDifferent(getSize(elem))
|
||||
}
|
||||
}
|
||||
|
||||
return { setElem, elemRef, sizeRef, ...size }
|
||||
}
|
|
@ -69,7 +69,9 @@ export function useQueryAndSortParams(options?: {
|
|||
if (router.isReady && !sort && shouldLoadFromStorage) {
|
||||
const localSort = localStorage.getItem(MARKETS_SORT) as Sort
|
||||
if (localSort) {
|
||||
setSort(localSort)
|
||||
router.query.s = localSort
|
||||
// Use replace to not break navigating back.
|
||||
router.replace(router, undefined, { shallow: true })
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
import { useState, useEffect } from 'react'
|
||||
import { PrivateUser, User } from '../../common/user'
|
||||
import { listenForAllUsers, listenForPrivateUsers } from '../lib/firebase/users'
|
||||
import {
|
||||
getUser,
|
||||
listenForAllUsers,
|
||||
listenForPrivateUsers,
|
||||
} from '../lib/firebase/users'
|
||||
|
||||
export const useUsers = () => {
|
||||
const [users, setUsers] = useState<User[]>([])
|
||||
|
@ -12,6 +16,18 @@ export const useUsers = () => {
|
|||
return users
|
||||
}
|
||||
|
||||
export const useUserById = (userId?: string) => {
|
||||
const [user, setUser] = useState<User | undefined>(undefined)
|
||||
|
||||
useEffect(() => {
|
||||
if (userId) {
|
||||
getUser(userId).then(setUser)
|
||||
}
|
||||
}, [userId])
|
||||
|
||||
return user
|
||||
}
|
||||
|
||||
export const usePrivateUsers = () => {
|
||||
const [users, setUsers] = useState<PrivateUser[]>([])
|
||||
|
||||
|
|
|
@ -22,7 +22,6 @@ import { getDpmProbability } from '../../../common/calculate-dpm'
|
|||
import { createRNG, shuffle } from '../../../common/util/random'
|
||||
import { getCpmmProbability } from '../../../common/calculate-cpmm'
|
||||
import { formatMoney, formatPercent } from '../../../common/util/format'
|
||||
import { getCpmmLiquidity } from '../../../common/calculate-cpmm'
|
||||
export type { Contract }
|
||||
|
||||
export function contractPath(contract: Contract) {
|
||||
|
@ -30,9 +29,7 @@ export function contractPath(contract: Contract) {
|
|||
}
|
||||
|
||||
export function contractMetrics(contract: Contract) {
|
||||
const { pool, createdTime, resolutionTime, isResolved } = contract
|
||||
|
||||
const truePool = _.sum(Object.values(pool))
|
||||
const { createdTime, resolutionTime, isResolved } = contract
|
||||
|
||||
const createdDate = dayjs(createdTime).format('MMM D')
|
||||
|
||||
|
@ -40,14 +37,9 @@ export function contractMetrics(contract: Contract) {
|
|||
? dayjs(resolutionTime).format('MMM D')
|
||||
: undefined
|
||||
|
||||
const liquidityLabel =
|
||||
contract.mechanism === 'dpm-2'
|
||||
? `${formatMoney(truePool)} pool`
|
||||
: `${formatMoney(
|
||||
contract.totalLiquidity ?? getCpmmLiquidity(pool, contract.p)
|
||||
)} liquidity`
|
||||
const volumeLabel = `${formatMoney(contract.volume)} volume`
|
||||
|
||||
return { truePool, liquidityLabel, createdDate, resolvedDate }
|
||||
return { volumeLabel, createdDate, resolvedDate }
|
||||
}
|
||||
|
||||
export function getBinaryProb(contract: FullContract<any, Binary>) {
|
||||
|
|
|
@ -126,6 +126,16 @@ export async function uploadData(
|
|||
return await getDownloadURL(uploadRef)
|
||||
}
|
||||
|
||||
export async function listUsers(userIds: string[]) {
|
||||
if (userIds.length > 10) {
|
||||
throw new Error('Too many users requested at once; Firestore limits to 10')
|
||||
}
|
||||
const userCollection = collection(db, 'users')
|
||||
const q = query(userCollection, where('id', 'in', userIds))
|
||||
const docs = await getDocs(q)
|
||||
return docs.docs.map((doc) => doc.data() as User)
|
||||
}
|
||||
|
||||
export async function listAllUsers() {
|
||||
const userCollection = collection(db, 'users')
|
||||
const q = query(userCollection)
|
||||
|
|
38
web/lib/util/copy.ts
Normal file
38
web/lib/util/copy.ts
Normal file
|
@ -0,0 +1,38 @@
|
|||
// From: https://stackoverflow.com/a/33928558/1592933
|
||||
// Copies a string to the clipboard. Must be called from within an
|
||||
// event handler such as click. May return false if it failed, but
|
||||
// this is not always possible. Browser support for Chrome 43+,
|
||||
// Firefox 42+, Safari 10+, Edge and Internet Explorer 10+.
|
||||
// Internet Explorer: The clipboard feature may be disabled by
|
||||
// an administrator. By default a prompt is shown the first
|
||||
// time the clipboard is used (per session).
|
||||
export function copyToClipboard(text: string) {
|
||||
if (navigator.clipboard) {
|
||||
navigator.clipboard.writeText(text)
|
||||
} else if (
|
||||
(window as any).clipboardData &&
|
||||
(window as any).clipboardData.setData
|
||||
) {
|
||||
console.log('copy 2')
|
||||
// Internet Explorer-specific code path to prevent textarea being shown while dialog is visible.
|
||||
return (window as any).clipboardData.setData('Text', text)
|
||||
} else if (
|
||||
document.queryCommandSupported &&
|
||||
document.queryCommandSupported('copy')
|
||||
) {
|
||||
console.log('copy 3')
|
||||
var textarea = document.createElement('textarea')
|
||||
textarea.textContent = text
|
||||
textarea.style.position = 'fixed' // Prevent scrolling to bottom of page in Microsoft Edge.
|
||||
document.body.appendChild(textarea)
|
||||
textarea.select()
|
||||
try {
|
||||
return document.execCommand('copy') // Security exception may be thrown by some browsers.
|
||||
} catch (ex) {
|
||||
console.warn('Copy to clipboard failed.', ex)
|
||||
return prompt('Copy to clipboard: Ctrl+C, Enter', text)
|
||||
} finally {
|
||||
document.body.removeChild(textarea)
|
||||
}
|
||||
}
|
||||
}
|
15
web/next-sitemap.js
Normal file
15
web/next-sitemap.js
Normal file
|
@ -0,0 +1,15 @@
|
|||
/** @type {import('next-sitemap').IConfig} */
|
||||
|
||||
module.exports = {
|
||||
siteUrl: process.env.SITE_URL || 'https://manifold.markets',
|
||||
changefreq: 'hourly',
|
||||
priority: 0.7, // Set high priority by default
|
||||
exclude: ['/admin', '/server-sitemap.xml'],
|
||||
generateRobotsTxt: true,
|
||||
robotsTxtOptions: {
|
||||
additionalSitemaps: [
|
||||
'https://manifold.markets/server-sitemap.xml', // <==== Add here
|
||||
],
|
||||
},
|
||||
// Other options: https://github.com/iamvishnusankar/next-sitemap#configuration-options
|
||||
}
|
|
@ -1,5 +1,4 @@
|
|||
const API_DOCS_URL =
|
||||
'https://manifoldmarkets.notion.site/Manifold-Markets-API-5e7d0aef4dcf452bb04b319e178fabc5'
|
||||
const API_DOCS_URL = 'https://docs.manifold.markets/api'
|
||||
|
||||
/** @type {import('next').NextConfig} */
|
||||
module.exports = {
|
||||
|
|
|
@ -12,7 +12,8 @@
|
|||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"format": "npx prettier --write .",
|
||||
"prepare": "cd .. && husky install web/.husky"
|
||||
"prepare": "cd .. && husky install web/.husky",
|
||||
"postbuild": "next-sitemap"
|
||||
},
|
||||
"dependencies": {
|
||||
"@headlessui/react": "1.4.2",
|
||||
|
@ -44,6 +45,7 @@
|
|||
"eslint-config-next": "12.0.4",
|
||||
"husky": "7.0.4",
|
||||
"lint-staged": "12.1.3",
|
||||
"next-sitemap": "^2.5.14",
|
||||
"postcss": "8.3.5",
|
||||
"prettier": "2.5.0",
|
||||
"prettier-plugin-tailwindcss": "^0.1.5",
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React from 'react'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
|
||||
import { useContractWithPreload } from '../../hooks/use-contract'
|
||||
import { ContractOverview } from '../../components/contract-overview'
|
||||
|
@ -10,7 +10,7 @@ import { ContractBetsTable, MyBetsSummary } from '../../components/bets-list'
|
|||
import { useBets } from '../../hooks/use-bets'
|
||||
import { Title } from '../../components/title'
|
||||
import { Spacer } from '../../components/layout/spacer'
|
||||
import { User } from '../../lib/firebase/users'
|
||||
import { listUsers, User } from '../../lib/firebase/users'
|
||||
import {
|
||||
Contract,
|
||||
getContractFromSlug,
|
||||
|
@ -30,6 +30,12 @@ import { listAllAnswers } from '../../lib/firebase/answers'
|
|||
import { Answer } from '../../../common/answer'
|
||||
import { AnswersPanel } from '../../components/answers/answers-panel'
|
||||
import { fromPropz, usePropz } from '../../hooks/use-propz'
|
||||
import { Leaderboard } from '../../components/leaderboard'
|
||||
import _ from 'lodash'
|
||||
import { calculatePayout, resolvedPayout } from '../../../common/calculate'
|
||||
import { formatMoney } from '../../../common/util/format'
|
||||
import { FeedBet, FeedComment } from '../../components/feed/feed-items'
|
||||
import { useUserById } from '../../hooks/use-users'
|
||||
|
||||
export const getStaticProps = fromPropz(getStaticPropz)
|
||||
export async function getStaticPropz(props: {
|
||||
|
@ -147,6 +153,19 @@ export default function ContractPage(props: {
|
|||
)}
|
||||
</ContractOverview>
|
||||
|
||||
{contract.isResolved && (
|
||||
<>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2">
|
||||
<ContractLeaderboard contract={contract} bets={bets} />
|
||||
<ContractTopTrades
|
||||
contract={contract}
|
||||
bets={bets}
|
||||
comments={comments}
|
||||
/>
|
||||
</div>
|
||||
<Spacer h={12} />
|
||||
</>
|
||||
)}
|
||||
<BetsSection contract={contract} user={user ?? null} bets={bets} />
|
||||
</div>
|
||||
|
||||
|
@ -195,6 +214,122 @@ function BetsSection(props: {
|
|||
)
|
||||
}
|
||||
|
||||
function ContractLeaderboard(props: { contract: Contract; bets: Bet[] }) {
|
||||
const { contract, bets } = props
|
||||
const [users, setUsers] = useState<User[]>()
|
||||
|
||||
// Create a map of userIds to total profits (including sales)
|
||||
const betsByUser = _.groupBy(bets, 'userId')
|
||||
const userProfits = _.mapValues(betsByUser, (bets) =>
|
||||
_.sumBy(bets, (bet) => resolvedPayout(contract, bet) - bet.amount)
|
||||
)
|
||||
|
||||
// Find the 5 users with the most profits
|
||||
const top5Ids = _.entries(userProfits)
|
||||
.sort(([i1, p1], [i2, p2]) => p2 - p1)
|
||||
.filter(([, p]) => p > 0)
|
||||
.slice(0, 5)
|
||||
.map(([id]) => id)
|
||||
|
||||
useEffect(() => {
|
||||
if (top5Ids.length > 0) {
|
||||
listUsers(top5Ids).then((users) => {
|
||||
const sortedUsers = _.sortBy(users, (user) => -userProfits[user.id])
|
||||
setUsers(sortedUsers)
|
||||
})
|
||||
}
|
||||
}, [])
|
||||
|
||||
return users && users.length > 0 ? (
|
||||
<Leaderboard
|
||||
title="🏅 Top traders"
|
||||
users={users || []}
|
||||
columns={[
|
||||
{
|
||||
header: 'Total profit',
|
||||
renderCell: (user) => formatMoney(userProfits[user.id] || 0),
|
||||
},
|
||||
]}
|
||||
className="mt-12 max-w-sm"
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
|
||||
function ContractTopTrades(props: {
|
||||
contract: Contract
|
||||
bets: Bet[]
|
||||
comments: Comment[]
|
||||
}) {
|
||||
const { contract, bets, comments } = props
|
||||
const commentsById = _.keyBy(comments, 'id')
|
||||
const betsById = _.keyBy(bets, 'id')
|
||||
|
||||
// If 'id2' is the sale of 'id1', both are logged with (id2 - id1) of profit
|
||||
// Otherwise, we record the profit at resolution time
|
||||
const profitById: Record<string, number> = {}
|
||||
for (const bet of bets) {
|
||||
if (bet.sale) {
|
||||
const originalBet = betsById[bet.sale.betId]
|
||||
const profit = bet.sale.amount - originalBet.amount
|
||||
profitById[bet.id] = profit
|
||||
profitById[originalBet.id] = profit
|
||||
} else {
|
||||
profitById[bet.id] = resolvedPayout(contract, bet) - bet.amount
|
||||
}
|
||||
}
|
||||
|
||||
// Now find the betId with the highest profit
|
||||
const topBetId = _.sortBy(bets, (b) => -profitById[b.id])[0]?.id
|
||||
const topBettor = useUserById(betsById[topBetId]?.userId)
|
||||
|
||||
// And also the commentId of the comment with the highest profit
|
||||
const topCommentId = _.sortBy(comments, (c) => -profitById[c.betId])[0]?.id
|
||||
|
||||
return (
|
||||
<div className="mt-12 max-w-sm">
|
||||
{topCommentId && profitById[topCommentId] > 0 && (
|
||||
<>
|
||||
<Title text="💬 Proven correct" className="!mt-0" />
|
||||
<div className="relative flex items-start space-x-3 rounded-md bg-gray-50 px-2 py-4">
|
||||
<FeedComment
|
||||
contract={contract}
|
||||
comment={commentsById[topCommentId]}
|
||||
bet={betsById[topCommentId]}
|
||||
hideOutcome={false}
|
||||
truncate={false}
|
||||
smallAvatar={false}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-2 text-sm text-gray-500">
|
||||
{commentsById[topCommentId].userName} made{' '}
|
||||
{formatMoney(profitById[topCommentId] || 0)}!
|
||||
</div>
|
||||
<Spacer h={16} />
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* If they're the same, only show the comment; otherwise show both */}
|
||||
{topBettor && topBetId !== topCommentId && profitById[topBetId] > 0 && (
|
||||
<>
|
||||
<Title text="💸 Smartest money" className="!mt-0" />
|
||||
<div className="relative flex items-start space-x-3 rounded-md bg-gray-50 px-2 py-4">
|
||||
<FeedBet
|
||||
contract={contract}
|
||||
bet={betsById[topBetId]}
|
||||
hideOutcome={false}
|
||||
smallAvatar={false}
|
||||
bettor={topBettor}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-2 text-sm text-gray-500">
|
||||
{topBettor?.name} made {formatMoney(profitById[topBetId] || 0)}!
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const getOpenGraphProps = (contract: Contract) => {
|
||||
const { resolution, question, creatorName, creatorUsername, outcomeType } =
|
||||
contract
|
||||
|
|
|
@ -1,7 +1,12 @@
|
|||
import clsx from 'clsx'
|
||||
import dayjs from 'dayjs'
|
||||
import _ from 'lodash'
|
||||
import { useState } from 'react'
|
||||
import { IS_PRIVATE_MANIFOLD } from '../../common/envs/constants'
|
||||
import { DailyCountChart } from '../components/analytics/charts'
|
||||
import {
|
||||
DailyCountChart,
|
||||
DailyPercentChart,
|
||||
} from '../components/analytics/charts'
|
||||
import { Col } from '../components/layout/col'
|
||||
import { Spacer } from '../components/layout/spacer'
|
||||
import { Page } from '../components/page'
|
||||
|
@ -13,7 +18,7 @@ import { getDailyContracts } from '../lib/firebase/contracts'
|
|||
|
||||
export const getStaticProps = fromPropz(getStaticPropz)
|
||||
export async function getStaticPropz() {
|
||||
const numberOfDays = 80
|
||||
const numberOfDays = 90
|
||||
const today = dayjs(dayjs().format('YYYY-MM-DD'))
|
||||
const startDate = today.subtract(numberOfDays, 'day')
|
||||
|
||||
|
@ -29,22 +34,70 @@ export async function getStaticPropz() {
|
|||
)
|
||||
const dailyCommentCounts = dailyComments.map((comments) => comments.length)
|
||||
|
||||
const dailyActiveUsers = _.zip(dailyContracts, dailyBets, dailyComments).map(
|
||||
const dailyUserIds = _.zip(dailyContracts, dailyBets, dailyComments).map(
|
||||
([contracts, bets, comments]) => {
|
||||
const creatorIds = (contracts ?? []).map((c) => c.creatorId)
|
||||
const betUserIds = (bets ?? []).map((bet) => bet.userId)
|
||||
const commentUserIds = (comments ?? []).map((comment) => comment.userId)
|
||||
return _.uniq([...creatorIds, ...betUserIds, commentUserIds]).length
|
||||
return _.uniq([...creatorIds, ...betUserIds, ...commentUserIds])
|
||||
}
|
||||
)
|
||||
|
||||
const dailyActiveUsers = dailyUserIds.map((userIds) => userIds.length)
|
||||
|
||||
const weeklyActiveUsers = dailyUserIds.map((_, i) => {
|
||||
const start = Math.max(0, i - 6)
|
||||
const end = i
|
||||
const uniques = new Set<string>()
|
||||
for (let j = start; j <= end; j++)
|
||||
dailyUserIds[j].forEach((userId) => uniques.add(userId))
|
||||
return uniques.size
|
||||
})
|
||||
|
||||
const monthlyActiveUsers = dailyUserIds.map((_, i) => {
|
||||
const start = Math.max(0, i - 30)
|
||||
const end = i
|
||||
const uniques = new Set<string>()
|
||||
for (let j = start; j <= end; j++)
|
||||
dailyUserIds[j].forEach((userId) => uniques.add(userId))
|
||||
return uniques.size
|
||||
})
|
||||
|
||||
const weekOnWeekRetention = dailyUserIds.map((_userId, i) => {
|
||||
const twoWeeksAgo = {
|
||||
start: Math.max(0, i - 13),
|
||||
end: Math.max(0, i - 7),
|
||||
}
|
||||
const lastWeek = {
|
||||
start: Math.max(0, i - 6),
|
||||
end: i,
|
||||
}
|
||||
|
||||
const activeTwoWeeksAgo = new Set<string>()
|
||||
for (let j = twoWeeksAgo.start; j <= twoWeeksAgo.end; j++) {
|
||||
dailyUserIds[j].forEach((userId) => activeTwoWeeksAgo.add(userId))
|
||||
}
|
||||
const activeLastWeek = new Set<string>()
|
||||
for (let j = lastWeek.start; j <= lastWeek.end; j++) {
|
||||
dailyUserIds[j].forEach((userId) => activeLastWeek.add(userId))
|
||||
}
|
||||
const retainedCount = _.sumBy(Array.from(activeTwoWeeksAgo), (userId) =>
|
||||
activeLastWeek.has(userId) ? 1 : 0
|
||||
)
|
||||
const retainedFrac = retainedCount / activeTwoWeeksAgo.size
|
||||
return Math.round(retainedFrac * 100 * 100) / 100
|
||||
})
|
||||
|
||||
return {
|
||||
props: {
|
||||
startDate: startDate.valueOf(),
|
||||
dailyActiveUsers,
|
||||
weeklyActiveUsers,
|
||||
monthlyActiveUsers,
|
||||
dailyBetCounts,
|
||||
dailyContractCounts,
|
||||
dailyCommentCounts,
|
||||
weekOnWeekRetention,
|
||||
},
|
||||
revalidate: 12 * 60 * 60, // regenerate after half a day
|
||||
}
|
||||
|
@ -53,16 +106,22 @@ export async function getStaticPropz() {
|
|||
export default function Analytics(props: {
|
||||
startDate: number
|
||||
dailyActiveUsers: number[]
|
||||
weeklyActiveUsers: number[]
|
||||
monthlyActiveUsers: number[]
|
||||
dailyBetCounts: number[]
|
||||
dailyContractCounts: number[]
|
||||
dailyCommentCounts: number[]
|
||||
weekOnWeekRetention: number[]
|
||||
}) {
|
||||
props = usePropz(props, getStaticPropz) ?? {
|
||||
startDate: 0,
|
||||
dailyActiveUsers: [],
|
||||
weeklyActiveUsers: [],
|
||||
monthlyActiveUsers: [],
|
||||
dailyBetCounts: [],
|
||||
dailyContractCounts: [],
|
||||
dailyCommentCounts: [],
|
||||
weekOnWeekRetention: [],
|
||||
}
|
||||
return (
|
||||
<Page>
|
||||
|
@ -73,12 +132,15 @@ export default function Analytics(props: {
|
|||
)
|
||||
}
|
||||
|
||||
function CustomAnalytics(props: {
|
||||
export function CustomAnalytics(props: {
|
||||
startDate: number
|
||||
dailyActiveUsers: number[]
|
||||
weeklyActiveUsers: number[]
|
||||
monthlyActiveUsers: number[]
|
||||
dailyBetCounts: number[]
|
||||
dailyContractCounts: number[]
|
||||
dailyCommentCounts: number[]
|
||||
weekOnWeekRetention: number[]
|
||||
}) {
|
||||
const {
|
||||
startDate,
|
||||
|
@ -86,46 +148,165 @@ function CustomAnalytics(props: {
|
|||
dailyBetCounts,
|
||||
dailyContractCounts,
|
||||
dailyCommentCounts,
|
||||
weeklyActiveUsers,
|
||||
monthlyActiveUsers,
|
||||
weekOnWeekRetention,
|
||||
} = props
|
||||
return (
|
||||
<Col>
|
||||
<Col className="px-2 sm:px-0">
|
||||
<Title text="Active users" />
|
||||
<DailyCountChart dailyCounts={dailyActiveUsers} startDate={startDate} />
|
||||
<p className="text-gray-500">
|
||||
An active user is a user who has traded in, commented on, or created a
|
||||
market.
|
||||
</p>
|
||||
<Spacer h={4} />
|
||||
|
||||
<Title text="Bets count" />
|
||||
<DailyCountChart
|
||||
dailyCounts={dailyBetCounts}
|
||||
<Tabs
|
||||
defaultIndex={1}
|
||||
tabs={[
|
||||
{
|
||||
title: 'Daily',
|
||||
content: (
|
||||
<DailyCountChart
|
||||
dailyCounts={dailyActiveUsers}
|
||||
startDate={startDate}
|
||||
small
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Weekly',
|
||||
content: (
|
||||
<DailyCountChart
|
||||
dailyCounts={weeklyActiveUsers}
|
||||
startDate={startDate}
|
||||
small
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Monthly',
|
||||
content: (
|
||||
<DailyCountChart
|
||||
dailyCounts={monthlyActiveUsers}
|
||||
startDate={startDate}
|
||||
small
|
||||
/>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<Spacer h={8} />
|
||||
|
||||
<Title text="Week-on-week retention" />
|
||||
<p className="text-gray-500">
|
||||
Out of all active users 2 weeks ago, how many came back last week?
|
||||
</p>
|
||||
<DailyPercentChart
|
||||
dailyPercent={weekOnWeekRetention}
|
||||
startDate={startDate}
|
||||
small
|
||||
/>
|
||||
<Spacer h={8} />
|
||||
|
||||
<Title text="Markets count" />
|
||||
<DailyCountChart
|
||||
dailyCounts={dailyContractCounts}
|
||||
startDate={startDate}
|
||||
small
|
||||
/>
|
||||
|
||||
<Title text="Comments count" />
|
||||
<DailyCountChart
|
||||
dailyCounts={dailyCommentCounts}
|
||||
startDate={startDate}
|
||||
small
|
||||
<Title text="Daily activity" />
|
||||
<Tabs
|
||||
defaultIndex={0}
|
||||
tabs={[
|
||||
{
|
||||
title: 'Trades',
|
||||
content: (
|
||||
<DailyCountChart
|
||||
dailyCounts={dailyBetCounts}
|
||||
startDate={startDate}
|
||||
small
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Markets created',
|
||||
content: (
|
||||
<DailyCountChart
|
||||
dailyCounts={dailyContractCounts}
|
||||
startDate={startDate}
|
||||
small
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Comments',
|
||||
content: (
|
||||
<DailyCountChart
|
||||
dailyCounts={dailyCommentCounts}
|
||||
startDate={startDate}
|
||||
small
|
||||
/>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<Spacer h={8} />
|
||||
</Col>
|
||||
)
|
||||
}
|
||||
|
||||
function FirebaseAnalytics() {
|
||||
// Edit dashboard at https://datastudio.google.com/u/0/reporting/faeaf3a4-c8da-4275-b157-98dad017d305/page/Gg3/edit
|
||||
type Tab = {
|
||||
title: string
|
||||
content: JSX.Element
|
||||
}
|
||||
|
||||
function Tabs(props: { tabs: Tab[]; defaultIndex: number }) {
|
||||
const { tabs, defaultIndex } = props
|
||||
const [activeTab, setActiveTab] = useState(tabs[defaultIndex])
|
||||
|
||||
return (
|
||||
<iframe
|
||||
className="w-full"
|
||||
height={2200}
|
||||
src="https://datastudio.google.com/embed/reporting/faeaf3a4-c8da-4275-b157-98dad017d305/page/Gg3"
|
||||
frameBorder="0"
|
||||
style={{ border: 0 }}
|
||||
allowFullScreen
|
||||
/>
|
||||
<div>
|
||||
<nav className="flex space-x-4" aria-label="Tabs">
|
||||
{tabs.map((tab) => (
|
||||
<a
|
||||
key={tab.title}
|
||||
href="#"
|
||||
className={clsx(
|
||||
tab.title === activeTab.title
|
||||
? 'bg-gray-100 text-gray-700'
|
||||
: 'text-gray-500 hover:text-gray-700',
|
||||
'rounded-md px-3 py-2 text-sm font-medium'
|
||||
)}
|
||||
aria-current={tab.title === activeTab.title ? 'page' : undefined}
|
||||
onClick={(e) => {
|
||||
console.log('clicked')
|
||||
e.preventDefault()
|
||||
setActiveTab(tab)
|
||||
}}
|
||||
>
|
||||
{tab.title}
|
||||
</a>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<div className="mt-4">{activeTab.content}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function FirebaseAnalytics() {
|
||||
// Edit dashboard at https://datastudio.google.com/u/0/reporting/faeaf3a4-c8da-4275-b157-98dad017d305/page/Gg3/edit
|
||||
|
||||
return (
|
||||
<>
|
||||
<Title text="Google Analytics" />
|
||||
<p className="text-gray-500">
|
||||
Less accurate; includes all viewers (not just signed-in users).
|
||||
</p>
|
||||
<Spacer h={4} />
|
||||
<iframe
|
||||
className="w-full"
|
||||
height={2200}
|
||||
src="https://datastudio.google.com/embed/reporting/faeaf3a4-c8da-4275-b157-98dad017d305/page/Gg3"
|
||||
frameBorder="0"
|
||||
style={{ border: 0 }}
|
||||
allowFullScreen
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -184,7 +184,7 @@ export function NewContract(props: { question: string; tag?: string }) {
|
|||
<InfoTooltip text="Optional. Describe how you will resolve this market." />
|
||||
</label>
|
||||
<Textarea
|
||||
className="textarea textarea-bordered w-full"
|
||||
className="textarea textarea-bordered w-full resize-none"
|
||||
rows={3}
|
||||
maxLength={MAX_DESCRIPTION_LENGTH}
|
||||
placeholder={descriptionPlaceholder}
|
||||
|
|
145
web/pages/embed/[username]/[contractSlug].tsx
Normal file
145
web/pages/embed/[username]/[contractSlug].tsx
Normal file
|
@ -0,0 +1,145 @@
|
|||
import { Bet } from '../../../../common/bet'
|
||||
import {
|
||||
Contract,
|
||||
DPM,
|
||||
FreeResponse,
|
||||
FullContract,
|
||||
} from '../../../../common/contract'
|
||||
import { DOMAIN } from '../../../../common/envs/constants'
|
||||
import { AnswersGraph } from '../../../components/answers/answers-graph'
|
||||
import {
|
||||
ResolutionOrChance,
|
||||
ContractDetails,
|
||||
} from '../../../components/contract-card'
|
||||
import { ContractProbGraph } from '../../../components/contract-prob-graph'
|
||||
import { Col } from '../../../components/layout/col'
|
||||
import { Row } from '../../../components/layout/row'
|
||||
import { Spacer } from '../../../components/layout/spacer'
|
||||
import { Linkify } from '../../../components/linkify'
|
||||
import { SiteLink } from '../../../components/site-link'
|
||||
import { useContractWithPreload } from '../../../hooks/use-contract'
|
||||
import { useMeasureSize } from '../../../hooks/use-measure-size'
|
||||
import { fromPropz, usePropz } from '../../../hooks/use-propz'
|
||||
import { useWindowSize } from '../../../hooks/use-window-size'
|
||||
import { listAllBets } from '../../../lib/firebase/bets'
|
||||
import {
|
||||
contractPath,
|
||||
getContractFromSlug,
|
||||
} from '../../../lib/firebase/contracts'
|
||||
import Custom404 from '../../404'
|
||||
|
||||
export const getStaticProps = fromPropz(getStaticPropz)
|
||||
export async function getStaticPropz(props: {
|
||||
params: { username: string; contractSlug: string }
|
||||
}) {
|
||||
const { username, contractSlug } = props.params
|
||||
const contract = (await getContractFromSlug(contractSlug)) || null
|
||||
const contractId = contract?.id
|
||||
|
||||
const bets = contractId ? await listAllBets(contractId) : []
|
||||
|
||||
return {
|
||||
props: {
|
||||
contract,
|
||||
username,
|
||||
slug: contractSlug,
|
||||
bets,
|
||||
},
|
||||
|
||||
revalidate: 60, // regenerate after a minute
|
||||
}
|
||||
}
|
||||
|
||||
export async function getStaticPaths() {
|
||||
return { paths: [], fallback: 'blocking' }
|
||||
}
|
||||
|
||||
export default function ContractEmbedPage(props: {
|
||||
contract: Contract | null
|
||||
username: string
|
||||
bets: Bet[]
|
||||
slug: string
|
||||
}) {
|
||||
props = usePropz(props, getStaticPropz) ?? {
|
||||
contract: null,
|
||||
username: '',
|
||||
bets: [],
|
||||
slug: '',
|
||||
}
|
||||
|
||||
const contract = useContractWithPreload(props.slug, props.contract)
|
||||
const { bets } = props
|
||||
|
||||
bets.sort((bet1, bet2) => bet1.createdTime - bet2.createdTime)
|
||||
|
||||
if (!contract) {
|
||||
return <Custom404 />
|
||||
}
|
||||
|
||||
return <ContractEmbed contract={contract} bets={bets} />
|
||||
}
|
||||
|
||||
function ContractEmbed(props: { contract: Contract; bets: Bet[] }) {
|
||||
const { contract, bets } = props
|
||||
const { question, resolution, outcomeType } = contract
|
||||
|
||||
const isBinary = outcomeType === 'BINARY'
|
||||
|
||||
const href = `https://${DOMAIN}${contractPath(contract)}`
|
||||
|
||||
const { height: windowHeight } = useWindowSize()
|
||||
const { setElem, height: topSectionHeight } = useMeasureSize()
|
||||
const paddingBottom = 8
|
||||
|
||||
const graphHeight =
|
||||
windowHeight && topSectionHeight
|
||||
? windowHeight - topSectionHeight - paddingBottom
|
||||
: 0
|
||||
|
||||
return (
|
||||
<Col className="w-full flex-1 bg-white">
|
||||
<div className="flex flex-col relative pt-2" ref={setElem}>
|
||||
<SiteLink
|
||||
className="absolute top-0 left-0 w-full h-full z-20"
|
||||
href={href}
|
||||
/>
|
||||
|
||||
<div className="px-3 text-xl md:text-2xl text-indigo-700">
|
||||
<Linkify text={question} />
|
||||
</div>
|
||||
|
||||
<Spacer h={3} />
|
||||
|
||||
<Row className="items-center justify-between gap-4 px-2">
|
||||
<ContractDetails
|
||||
contract={contract}
|
||||
isCreator={false}
|
||||
hideShareButtons
|
||||
/>
|
||||
|
||||
{(isBinary || resolution) && (
|
||||
<ResolutionOrChance contract={contract} />
|
||||
)}
|
||||
</Row>
|
||||
|
||||
<Spacer h={2} />
|
||||
</div>
|
||||
|
||||
<div className="mx-1" style={{ paddingBottom }}>
|
||||
{isBinary ? (
|
||||
<ContractProbGraph
|
||||
contract={contract}
|
||||
bets={bets}
|
||||
height={graphHeight}
|
||||
/>
|
||||
) : (
|
||||
<AnswersGraph
|
||||
contract={contract as FullContract<DPM, FreeResponse>}
|
||||
bets={bets}
|
||||
height={graphHeight}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Col>
|
||||
)
|
||||
}
|
21
web/pages/embed/analytics.tsx
Normal file
21
web/pages/embed/analytics.tsx
Normal file
|
@ -0,0 +1,21 @@
|
|||
import { FirstArgument } from '../../../common/util/types'
|
||||
import { Col } from '../../components/layout/col'
|
||||
import { Spacer } from '../../components/layout/spacer'
|
||||
import { fromPropz } from '../../hooks/use-propz'
|
||||
import Analytics, {
|
||||
CustomAnalytics,
|
||||
FirebaseAnalytics,
|
||||
getStaticPropz,
|
||||
} from '../analytics'
|
||||
|
||||
export const getStaticProps = fromPropz(getStaticPropz)
|
||||
|
||||
export default function AnalyticsEmbed(props: FirstArgument<typeof Analytics>) {
|
||||
return (
|
||||
<Col className="w-full px-2 bg-white">
|
||||
<CustomAnalytics {...props} />
|
||||
<Spacer h={8} />
|
||||
<FirebaseAnalytics />
|
||||
</Col>
|
||||
)
|
||||
}
|
|
@ -93,8 +93,7 @@ export default function Folds(props: {
|
|||
|
||||
<div className="mb-6 text-gray-500">
|
||||
Communities on Manifold are centered around a collection of
|
||||
markets. Follow a community to personalize your feed and receive
|
||||
relevant updates.
|
||||
markets. Follow a community to personalize your feed!
|
||||
</div>
|
||||
|
||||
<input
|
||||
|
|
|
@ -6,6 +6,7 @@ import { Page } from '../components/page'
|
|||
import { FeedPromo } from '../components/feed-create'
|
||||
import { Col } from '../components/layout/col'
|
||||
import { useUser } from '../hooks/use-user'
|
||||
import { SiteLink } from '../components/site-link'
|
||||
|
||||
export async function getStaticProps() {
|
||||
const hotContracts = (await getHotContracts().catch(() => [])) ?? []
|
||||
|
@ -31,6 +32,12 @@ const Home = (props: { hotContracts: Contract[] }) => {
|
|||
<Col className="items-center">
|
||||
<Col className="max-w-3xl">
|
||||
<FeedPromo hotContracts={hotContracts ?? []} />
|
||||
<p className="mt-6 text-gray-500">
|
||||
View{' '}
|
||||
<SiteLink href="/markets" className="font-bold text-gray-700">
|
||||
all markets
|
||||
</SiteLink>
|
||||
</p>
|
||||
</Col>
|
||||
</Col>
|
||||
</Page>
|
||||
|
|
|
@ -195,7 +195,7 @@ ${TEST_VALUE}
|
|||
|
||||
<Textarea
|
||||
placeholder="e.g. This market is part of the ACX predictions for 2022..."
|
||||
className="input"
|
||||
className="input resize-none"
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value || '')}
|
||||
/>
|
||||
|
|
|
@ -41,7 +41,7 @@ function EditUserField(props: {
|
|||
|
||||
{field === 'bio' ? (
|
||||
<Textarea
|
||||
className="textarea textarea-bordered w-full"
|
||||
className="textarea textarea-bordered w-full resize-none"
|
||||
value={value}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
onBlur={updateField}
|
||||
|
|
21
web/pages/server-sitemap.xml.tsx
Normal file
21
web/pages/server-sitemap.xml.tsx
Normal file
|
@ -0,0 +1,21 @@
|
|||
import { GetServerSideProps } from 'next'
|
||||
import { getServerSideSitemap } from 'next-sitemap'
|
||||
import { DOMAIN } from '../../common/envs/constants'
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async (ctx) => {
|
||||
// Fetching data from https://docs.manifold.markets/api
|
||||
const response = await fetch(`https://${DOMAIN}/api/v0/markets`)
|
||||
|
||||
const liteMarkets = await response.json()
|
||||
const fields = liteMarkets.map((liteMarket: any) => ({
|
||||
// See https://www.sitemaps.org/protocol.html
|
||||
loc: liteMarket.url,
|
||||
changefreq: 'hourly',
|
||||
priority: 0.2, // Individual markets aren't that important
|
||||
// TODO: Add `lastmod` aka last modified time
|
||||
}))
|
||||
return getServerSideSitemap(ctx, fields)
|
||||
}
|
||||
|
||||
// Default export to prevent next.js errors
|
||||
export default function Sitemap() {}
|
10
web/public/robots.txt
Normal file
10
web/public/robots.txt
Normal file
|
@ -0,0 +1,10 @@
|
|||
# *
|
||||
User-agent: *
|
||||
Allow: /
|
||||
|
||||
# Host
|
||||
Host: https://manifold.markets
|
||||
|
||||
# Sitemaps
|
||||
Sitemap: https://manifold.markets/sitemap.xml
|
||||
Sitemap: https://manifold.markets/server-sitemap.xml
|
19
web/public/sitemap-0.xml
Normal file
19
web/public/sitemap-0.xml
Normal file
|
@ -0,0 +1,19 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:news="http://www.google.com/schemas/sitemap-news/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml" xmlns:mobile="http://www.google.com/schemas/sitemap-mobile/1.0" xmlns:image="http://www.google.com/schemas/sitemap-image/1.1" xmlns:video="http://www.google.com/schemas/sitemap-video/1.1">
|
||||
<url><loc>https://manifold.markets</loc><changefreq>hourly</changefreq><priority>0.7</priority><lastmod>2022-03-24T16:51:19.526Z</lastmod></url>
|
||||
<url><loc>https://manifold.markets/about</loc><changefreq>hourly</changefreq><priority>0.7</priority><lastmod>2022-03-24T16:51:19.526Z</lastmod></url>
|
||||
<url><loc>https://manifold.markets/account</loc><changefreq>hourly</changefreq><priority>0.7</priority><lastmod>2022-03-24T16:51:19.526Z</lastmod></url>
|
||||
<url><loc>https://manifold.markets/add-funds</loc><changefreq>hourly</changefreq><priority>0.7</priority><lastmod>2022-03-24T16:51:19.526Z</lastmod></url>
|
||||
<url><loc>https://manifold.markets/analytics</loc><changefreq>hourly</changefreq><priority>0.7</priority><lastmod>2022-03-24T16:51:19.526Z</lastmod></url>
|
||||
<url><loc>https://manifold.markets/create</loc><changefreq>hourly</changefreq><priority>0.7</priority><lastmod>2022-03-24T16:51:19.526Z</lastmod></url>
|
||||
<url><loc>https://manifold.markets/embed/analytics</loc><changefreq>hourly</changefreq><priority>0.7</priority><lastmod>2022-03-24T16:51:19.526Z</lastmod></url>
|
||||
<url><loc>https://manifold.markets/folds</loc><changefreq>hourly</changefreq><priority>0.7</priority><lastmod>2022-03-24T16:51:19.526Z</lastmod></url>
|
||||
<url><loc>https://manifold.markets/home</loc><changefreq>hourly</changefreq><priority>0.7</priority><lastmod>2022-03-24T16:51:19.526Z</lastmod></url>
|
||||
<url><loc>https://manifold.markets/landing-page</loc><changefreq>hourly</changefreq><priority>0.7</priority><lastmod>2022-03-24T16:51:19.526Z</lastmod></url>
|
||||
<url><loc>https://manifold.markets/leaderboards</loc><changefreq>hourly</changefreq><priority>0.7</priority><lastmod>2022-03-24T16:51:19.526Z</lastmod></url>
|
||||
<url><loc>https://manifold.markets/make-predictions</loc><changefreq>hourly</changefreq><priority>0.7</priority><lastmod>2022-03-24T16:51:19.526Z</lastmod></url>
|
||||
<url><loc>https://manifold.markets/markets</loc><changefreq>hourly</changefreq><priority>0.7</priority><lastmod>2022-03-24T16:51:19.526Z</lastmod></url>
|
||||
<url><loc>https://manifold.markets/profile</loc><changefreq>hourly</changefreq><priority>0.7</priority><lastmod>2022-03-24T16:51:19.526Z</lastmod></url>
|
||||
<url><loc>https://manifold.markets/simulator</loc><changefreq>hourly</changefreq><priority>0.7</priority><lastmod>2022-03-24T16:51:19.526Z</lastmod></url>
|
||||
<url><loc>https://manifold.markets/trades</loc><changefreq>hourly</changefreq><priority>0.7</priority><lastmod>2022-03-24T16:51:19.526Z</lastmod></url>
|
||||
</urlset>
|
4
web/public/sitemap.xml
Normal file
4
web/public/sitemap.xml
Normal file
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
<sitemap><loc>https://manifold.markets/sitemap-0.xml</loc></sitemap>
|
||||
</sitemapindex>
|
18
yarn.lock
18
yarn.lock
|
@ -72,6 +72,11 @@
|
|||
"@babel/helper-validator-identifier" "^7.14.9"
|
||||
to-fast-properties "^2.0.0"
|
||||
|
||||
"@corex/deepmerge@^2.6.148":
|
||||
version "2.6.148"
|
||||
resolved "https://registry.yarnpkg.com/@corex/deepmerge/-/deepmerge-2.6.148.tgz#8fa825d53ffd1cbcafce1b6a830eefd3dcc09dd5"
|
||||
integrity sha512-6QMz0/2h5C3ua51iAnXMPWFbb1QOU1UvSM4bKBw5mzdT+WtLgjbETBBIQZ+Sh9WvEcGwlAt/DEdRpIC3XlDBMA==
|
||||
|
||||
"@eslint/eslintrc@^0.4.3":
|
||||
version "0.4.3"
|
||||
resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-0.4.3.tgz#9e42981ef035beb3dd49add17acb96e8ff6f394c"
|
||||
|
@ -4322,6 +4327,11 @@ minimist@^1.1.1, minimist@^1.2.0:
|
|||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
|
||||
integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
|
||||
|
||||
minimist@^1.2.6:
|
||||
version "1.2.6"
|
||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
|
||||
integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==
|
||||
|
||||
mri@^1.1.5:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/mri/-/mri-1.2.0.tgz#6721480fec2a11a4889861115a48b6cbe7cc8f0b"
|
||||
|
@ -4373,6 +4383,14 @@ netmask@^1.0.6:
|
|||
resolved "https://registry.yarnpkg.com/netmask/-/netmask-1.0.6.tgz#20297e89d86f6f6400f250d9f4f6b4c1945fcd35"
|
||||
integrity sha1-ICl+idhvb2QA8lDZ9Pa0wZRfzTU=
|
||||
|
||||
next-sitemap@^2.5.14:
|
||||
version "2.5.14"
|
||||
resolved "https://registry.yarnpkg.com/next-sitemap/-/next-sitemap-2.5.14.tgz#f196c90d4aef8444c6eb7266875bf2179a515bb7"
|
||||
integrity sha512-aJmxGmoE23NClCi1P6KpHov9DUieF/ZZbfGpTiruOYCq4nKu8Q4masOuswlOl3nNKZa0C3u4JG+TPubjslYH9A==
|
||||
dependencies:
|
||||
"@corex/deepmerge" "^2.6.148"
|
||||
minimist "^1.2.6"
|
||||
|
||||
next@12.0.7:
|
||||
version "12.0.7"
|
||||
resolved "https://registry.yarnpkg.com/next/-/next-12.0.7.tgz#33ebf229b81b06e583ab5ae7613cffe1ca2103fc"
|
||||
|
|
Loading…
Reference in New Issue
Block a user