Merge branch 'main' into embed-sizing
This commit is contained in:
commit
241fc7a802
|
@ -5,4 +5,4 @@ export type Like = {
|
|||
createdTime: number
|
||||
tipTxnId?: string // only holds most recent tip txn id
|
||||
}
|
||||
export const LIKE_TIP_AMOUNT = 5
|
||||
export const LIKE_TIP_AMOUNT = 10
|
||||
|
|
|
@ -5,7 +5,6 @@ import { formatMoney } from 'common/util/format'
|
|||
import { Col } from './layout/col'
|
||||
import { SiteLink } from './site-link'
|
||||
import { ENV_CONFIG } from 'common/envs/constants'
|
||||
import { useWindowSize } from 'web/hooks/use-window-size'
|
||||
import { Row } from './layout/row'
|
||||
|
||||
export function AmountInput(props: {
|
||||
|
@ -36,9 +35,6 @@ export function AmountInput(props: {
|
|||
onChange(isInvalid ? undefined : amount)
|
||||
}
|
||||
|
||||
const { width } = useWindowSize()
|
||||
const isMobile = (width ?? 0) < 768
|
||||
|
||||
return (
|
||||
<>
|
||||
<Col className={className}>
|
||||
|
@ -50,7 +46,7 @@ export function AmountInput(props: {
|
|||
className={clsx(
|
||||
'placeholder:text-greyscale-4 border-greyscale-2 rounded-md pl-9',
|
||||
error && 'input-error',
|
||||
isMobile ? 'w-24' : '',
|
||||
'w-24 md:w-auto',
|
||||
inputClassName
|
||||
)}
|
||||
ref={inputRef}
|
||||
|
@ -59,7 +55,6 @@ export function AmountInput(props: {
|
|||
inputMode="numeric"
|
||||
placeholder="0"
|
||||
maxLength={6}
|
||||
autoFocus={!isMobile}
|
||||
value={amount ?? ''}
|
||||
disabled={disabled}
|
||||
onChange={(e) => onAmountChange(e.target.value)}
|
||||
|
|
|
@ -47,7 +47,6 @@ import { Modal } from './layout/modal'
|
|||
import { Title } from './title'
|
||||
import toast from 'react-hot-toast'
|
||||
import { CheckIcon } from '@heroicons/react/solid'
|
||||
import { useWindowSize } from 'web/hooks/use-window-size'
|
||||
|
||||
export function BetPanel(props: {
|
||||
contract: CPMMBinaryContract | PseudoNumericContract
|
||||
|
@ -179,12 +178,7 @@ export function BuyPanel(props: {
|
|||
const initialProb = getProbability(contract)
|
||||
const isPseudoNumeric = contract.outcomeType === 'PSEUDO_NUMERIC'
|
||||
|
||||
const windowSize = useWindowSize()
|
||||
const initialOutcome =
|
||||
windowSize.width && windowSize.width >= 1280 ? 'YES' : undefined
|
||||
const [outcome, setOutcome] = useState<'YES' | 'NO' | undefined>(
|
||||
initialOutcome
|
||||
)
|
||||
const [outcome, setOutcome] = useState<'YES' | 'NO' | undefined>()
|
||||
const [betAmount, setBetAmount] = useState<number | undefined>(10)
|
||||
const [error, setError] = useState<string | undefined>()
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
|
|
|
@ -46,7 +46,6 @@ export function Button(props: {
|
|||
<button
|
||||
type={type}
|
||||
className={clsx(
|
||||
className,
|
||||
'font-md items-center justify-center rounded-md border border-transparent shadow-sm transition-colors disabled:cursor-not-allowed',
|
||||
sizeClasses,
|
||||
color === 'green' &&
|
||||
|
@ -66,7 +65,8 @@ export function Button(props: {
|
|||
color === 'gray-white' &&
|
||||
'text-greyscale-6 hover:bg-greyscale-2 border-none shadow-none disabled:opacity-50',
|
||||
color === 'highlight-blue' &&
|
||||
'text-highlight-blue disabled:bg-greyscale-2 border-none shadow-none'
|
||||
'text-highlight-blue disabled:bg-greyscale-2 border-none shadow-none',
|
||||
className
|
||||
)}
|
||||
disabled={disabled}
|
||||
onClick={onClick}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { useMemo, useRef } from 'react'
|
||||
import { useMemo } from 'react'
|
||||
import { last, sortBy } from 'lodash'
|
||||
import { scaleTime, scaleLinear } from 'd3-scale'
|
||||
|
||||
|
@ -6,7 +6,6 @@ import { Bet } from 'common/bet'
|
|||
import { getProbability, getInitialProbability } from 'common/calculate'
|
||||
import { BinaryContract } from 'common/contract'
|
||||
import { DAY_MS } from 'common/util/time'
|
||||
import { useIsMobile } from 'web/hooks/use-is-mobile'
|
||||
import {
|
||||
TooltipProps,
|
||||
MARGIN_X,
|
||||
|
@ -17,7 +16,6 @@ import {
|
|||
formatPct,
|
||||
} from '../helpers'
|
||||
import { HistoryPoint, SingleValueHistoryChart } from '../generic-charts'
|
||||
import { useElementWidth } from 'web/hooks/use-element-width'
|
||||
import { Row } from 'web/components/layout/row'
|
||||
import { Avatar } from 'web/components/avatar'
|
||||
|
||||
|
@ -25,19 +23,19 @@ const getBetPoints = (bets: Bet[]) => {
|
|||
return sortBy(bets, (b) => b.createdTime).map((b) => ({
|
||||
x: new Date(b.createdTime),
|
||||
y: b.probAfter,
|
||||
datum: b,
|
||||
obj: b,
|
||||
}))
|
||||
}
|
||||
|
||||
const BinaryChartTooltip = (props: TooltipProps<HistoryPoint<Bet>>) => {
|
||||
const { p, xScale } = props
|
||||
const { x, y, datum } = p
|
||||
const BinaryChartTooltip = (props: TooltipProps<Date, HistoryPoint<Bet>>) => {
|
||||
const { data, mouseX, xScale } = props
|
||||
const [start, end] = xScale.domain()
|
||||
const d = xScale.invert(mouseX)
|
||||
return (
|
||||
<Row className="items-center gap-2">
|
||||
{datum && <Avatar size="xs" avatarUrl={datum.userAvatarUrl} />}
|
||||
<span className="font-semibold">{formatDateInRange(x, start, end)}</span>
|
||||
<span className="text-greyscale-6">{formatPct(y)}</span>
|
||||
{data.obj && <Avatar size="xs" avatarUrl={data.obj.userAvatarUrl} />}
|
||||
<span className="font-semibold">{formatDateInRange(d, start, end)}</span>
|
||||
<span className="text-greyscale-6">{formatPct(data.y)}</span>
|
||||
</Row>
|
||||
)
|
||||
}
|
||||
|
@ -45,10 +43,11 @@ const BinaryChartTooltip = (props: TooltipProps<HistoryPoint<Bet>>) => {
|
|||
export const BinaryContractChart = (props: {
|
||||
contract: BinaryContract
|
||||
bets: Bet[]
|
||||
height?: number
|
||||
width: number
|
||||
height: number
|
||||
onMouseOver?: (p: HistoryPoint<Bet> | undefined) => void
|
||||
}) => {
|
||||
const { contract, bets, onMouseOver } = props
|
||||
const { contract, bets, width, height, onMouseOver } = props
|
||||
const [start, end] = getDateRange(contract)
|
||||
const startP = getInitialProbability(contract)
|
||||
const endP = getProbability(contract)
|
||||
|
@ -67,28 +66,19 @@ export const BinaryContractChart = (props: {
|
|||
Date.now()
|
||||
)
|
||||
const visibleRange = [start, rightmostDate]
|
||||
const isMobile = useIsMobile(800)
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const width = useElementWidth(containerRef) ?? 0
|
||||
const height = props.height ?? (isMobile ? 150 : 250)
|
||||
const xScale = scaleTime(visibleRange, [0, width - MARGIN_X])
|
||||
const yScale = scaleLinear([0, 1], [height - MARGIN_Y, 0])
|
||||
|
||||
return (
|
||||
<div ref={containerRef}>
|
||||
{width > 0 && (
|
||||
<SingleValueHistoryChart
|
||||
w={width}
|
||||
h={height}
|
||||
xScale={xScale}
|
||||
yScale={yScale}
|
||||
data={data}
|
||||
color="#11b981"
|
||||
onMouseOver={onMouseOver}
|
||||
Tooltip={BinaryChartTooltip}
|
||||
pct
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<SingleValueHistoryChart
|
||||
w={width}
|
||||
h={height}
|
||||
xScale={xScale}
|
||||
yScale={yScale}
|
||||
data={data}
|
||||
color="#11b981"
|
||||
onMouseOver={onMouseOver}
|
||||
Tooltip={BinaryChartTooltip}
|
||||
pct
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { useMemo, useRef } from 'react'
|
||||
import { useMemo } from 'react'
|
||||
import { last, sum, sortBy, groupBy } from 'lodash'
|
||||
import { scaleTime, scaleLinear } from 'd3-scale'
|
||||
|
||||
|
@ -6,7 +6,6 @@ import { Bet } from 'common/bet'
|
|||
import { Answer } from 'common/answer'
|
||||
import { FreeResponseContract, MultipleChoiceContract } from 'common/contract'
|
||||
import { getOutcomeProbability } from 'common/calculate'
|
||||
import { useIsMobile } from 'web/hooks/use-is-mobile'
|
||||
import { DAY_MS } from 'common/util/time'
|
||||
import {
|
||||
TooltipProps,
|
||||
|
@ -18,7 +17,6 @@ import {
|
|||
formatDateInRange,
|
||||
} from '../helpers'
|
||||
import { MultiPoint, MultiValueHistoryChart } from '../generic-charts'
|
||||
import { useElementWidth } from 'web/hooks/use-element-width'
|
||||
import { Row } from 'web/components/layout/row'
|
||||
import { Avatar } from 'web/components/avatar'
|
||||
|
||||
|
@ -114,7 +112,7 @@ const getBetPoints = (answers: Answer[], bets: Bet[]) => {
|
|||
points.push({
|
||||
x: new Date(bet.createdTime),
|
||||
y: answers.map((a) => sharesByOutcome[a.id] ** 2 / sharesSquared),
|
||||
datum: bet,
|
||||
obj: bet,
|
||||
})
|
||||
}
|
||||
return points
|
||||
|
@ -146,10 +144,11 @@ const Legend = (props: { className?: string; items: LegendItem[] }) => {
|
|||
export const ChoiceContractChart = (props: {
|
||||
contract: FreeResponseContract | MultipleChoiceContract
|
||||
bets: Bet[]
|
||||
height?: number
|
||||
width: number
|
||||
height: number
|
||||
onMouseOver?: (p: MultiPoint<Bet> | undefined) => void
|
||||
}) => {
|
||||
const { contract, bets, onMouseOver } = props
|
||||
const { contract, bets, width, height, onMouseOver } = props
|
||||
const [start, end] = getDateRange(contract)
|
||||
const answers = useMemo(
|
||||
() => getTrackedAnswers(contract, CATEGORY_COLORS.length),
|
||||
|
@ -173,20 +172,16 @@ export const ChoiceContractChart = (props: {
|
|||
Date.now()
|
||||
)
|
||||
const visibleRange = [start, rightmostDate]
|
||||
const isMobile = useIsMobile(800)
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const width = useElementWidth(containerRef) ?? 0
|
||||
const height = props.height ?? (isMobile ? 250 : 350)
|
||||
const xScale = scaleTime(visibleRange, [0, width - MARGIN_X])
|
||||
const yScale = scaleLinear([0, 1], [height - MARGIN_Y, 0])
|
||||
|
||||
const ChoiceTooltip = useMemo(
|
||||
() => (props: TooltipProps<MultiPoint<Bet>>) => {
|
||||
const { p, xScale } = props
|
||||
const { x, y, datum } = p
|
||||
() => (props: TooltipProps<Date, MultiPoint<Bet>>) => {
|
||||
const { data, mouseX, xScale } = props
|
||||
const [start, end] = xScale.domain()
|
||||
const d = xScale.invert(mouseX)
|
||||
const legendItems = sortBy(
|
||||
y.map((p, i) => ({
|
||||
data.y.map((p, i) => ({
|
||||
color: CATEGORY_COLORS[i],
|
||||
label: answers[i].text,
|
||||
value: formatPct(p),
|
||||
|
@ -197,9 +192,11 @@ export const ChoiceContractChart = (props: {
|
|||
return (
|
||||
<>
|
||||
<Row className="items-center gap-2">
|
||||
{datum && <Avatar size="xxs" avatarUrl={datum.userAvatarUrl} />}
|
||||
{data.obj && (
|
||||
<Avatar size="xxs" avatarUrl={data.obj.userAvatarUrl} />
|
||||
)}
|
||||
<span className="text-semibold text-base">
|
||||
{formatDateInRange(x, start, end)}
|
||||
{formatDateInRange(d, start, end)}
|
||||
</span>
|
||||
</Row>
|
||||
<Legend className="max-w-xs" items={legendItems} />
|
||||
|
@ -210,20 +207,16 @@ export const ChoiceContractChart = (props: {
|
|||
)
|
||||
|
||||
return (
|
||||
<div ref={containerRef}>
|
||||
{width > 0 && (
|
||||
<MultiValueHistoryChart
|
||||
w={width}
|
||||
h={height}
|
||||
xScale={xScale}
|
||||
yScale={yScale}
|
||||
data={data}
|
||||
colors={CATEGORY_COLORS}
|
||||
onMouseOver={onMouseOver}
|
||||
Tooltip={ChoiceTooltip}
|
||||
pct
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<MultiValueHistoryChart
|
||||
w={width}
|
||||
h={height}
|
||||
xScale={xScale}
|
||||
yScale={yScale}
|
||||
data={data}
|
||||
colors={CATEGORY_COLORS}
|
||||
onMouseOver={onMouseOver}
|
||||
Tooltip={ChoiceTooltip}
|
||||
pct
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -8,7 +8,8 @@ import { NumericContractChart } from './numeric'
|
|||
export const ContractChart = (props: {
|
||||
contract: Contract
|
||||
bets: Bet[]
|
||||
height?: number
|
||||
width: number
|
||||
height: number
|
||||
}) => {
|
||||
const { contract } = props
|
||||
switch (contract.outcomeType) {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { useMemo, useRef } from 'react'
|
||||
import { useMemo } from 'react'
|
||||
import { range } from 'lodash'
|
||||
import { scaleLinear } from 'd3-scale'
|
||||
|
||||
|
@ -6,10 +6,8 @@ import { formatLargeNumber } from 'common/util/format'
|
|||
import { getDpmOutcomeProbabilities } from 'common/calculate-dpm'
|
||||
import { NumericContract } from 'common/contract'
|
||||
import { NUMERIC_GRAPH_COLOR } from 'common/numeric-constants'
|
||||
import { useIsMobile } from 'web/hooks/use-is-mobile'
|
||||
import { TooltipProps, MARGIN_X, MARGIN_Y, formatPct } from '../helpers'
|
||||
import { DistributionPoint, DistributionChart } from '../generic-charts'
|
||||
import { useElementWidth } from 'web/hooks/use-element-width'
|
||||
|
||||
const getNumericChartData = (contract: NumericContract) => {
|
||||
const { totalShares, bucketCount, min, max } = contract
|
||||
|
@ -21,45 +19,41 @@ const getNumericChartData = (contract: NumericContract) => {
|
|||
}))
|
||||
}
|
||||
|
||||
const NumericChartTooltip = (props: TooltipProps<DistributionPoint>) => {
|
||||
const { x, y } = props.p
|
||||
const NumericChartTooltip = (
|
||||
props: TooltipProps<number, DistributionPoint>
|
||||
) => {
|
||||
const { data, mouseX, xScale } = props
|
||||
const x = xScale.invert(mouseX)
|
||||
return (
|
||||
<>
|
||||
<span className="text-semibold">{formatLargeNumber(x)}</span>
|
||||
<span className="text-greyscale-6">{formatPct(y, 2)}</span>
|
||||
<span className="text-greyscale-6">{formatPct(data.y, 2)}</span>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export const NumericContractChart = (props: {
|
||||
contract: NumericContract
|
||||
height?: number
|
||||
width: number
|
||||
height: number
|
||||
onMouseOver?: (p: DistributionPoint | undefined) => void
|
||||
}) => {
|
||||
const { contract, onMouseOver } = props
|
||||
const { contract, width, height, onMouseOver } = props
|
||||
const { min, max } = contract
|
||||
const data = useMemo(() => getNumericChartData(contract), [contract])
|
||||
const isMobile = useIsMobile(800)
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const width = useElementWidth(containerRef) ?? 0
|
||||
const height = props.height ?? (isMobile ? 150 : 250)
|
||||
const maxY = Math.max(...data.map((d) => d.y))
|
||||
const xScale = scaleLinear([min, max], [0, width - MARGIN_X])
|
||||
const yScale = scaleLinear([0, maxY], [height - MARGIN_Y, 0])
|
||||
return (
|
||||
<div ref={containerRef}>
|
||||
{width > 0 && (
|
||||
<DistributionChart
|
||||
w={width}
|
||||
h={height}
|
||||
xScale={xScale}
|
||||
yScale={yScale}
|
||||
data={data}
|
||||
color={NUMERIC_GRAPH_COLOR}
|
||||
onMouseOver={onMouseOver}
|
||||
Tooltip={NumericChartTooltip}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<DistributionChart
|
||||
w={width}
|
||||
h={height}
|
||||
xScale={xScale}
|
||||
yScale={yScale}
|
||||
data={data}
|
||||
color={NUMERIC_GRAPH_COLOR}
|
||||
onMouseOver={onMouseOver}
|
||||
Tooltip={NumericChartTooltip}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { useMemo, useRef } from 'react'
|
||||
import { useMemo } from 'react'
|
||||
import { last, sortBy } from 'lodash'
|
||||
import { scaleTime, scaleLog, scaleLinear } from 'd3-scale'
|
||||
|
||||
|
@ -8,7 +8,6 @@ import { getInitialProbability, getProbability } from 'common/calculate'
|
|||
import { formatLargeNumber } from 'common/util/format'
|
||||
import { PseudoNumericContract } from 'common/contract'
|
||||
import { NUMERIC_GRAPH_COLOR } from 'common/numeric-constants'
|
||||
import { useIsMobile } from 'web/hooks/use-is-mobile'
|
||||
import {
|
||||
TooltipProps,
|
||||
MARGIN_X,
|
||||
|
@ -18,7 +17,6 @@ import {
|
|||
formatDateInRange,
|
||||
} from '../helpers'
|
||||
import { HistoryPoint, SingleValueHistoryChart } from '../generic-charts'
|
||||
import { useElementWidth } from 'web/hooks/use-element-width'
|
||||
import { Row } from 'web/components/layout/row'
|
||||
import { Avatar } from 'web/components/avatar'
|
||||
|
||||
|
@ -37,19 +35,21 @@ const getBetPoints = (bets: Bet[], scaleP: (p: number) => number) => {
|
|||
return sortBy(bets, (b) => b.createdTime).map((b) => ({
|
||||
x: new Date(b.createdTime),
|
||||
y: scaleP(b.probAfter),
|
||||
datum: b,
|
||||
obj: b,
|
||||
}))
|
||||
}
|
||||
|
||||
const PseudoNumericChartTooltip = (props: TooltipProps<HistoryPoint<Bet>>) => {
|
||||
const { p, xScale } = props
|
||||
const { x, y, datum } = p
|
||||
const PseudoNumericChartTooltip = (
|
||||
props: TooltipProps<Date, HistoryPoint<Bet>>
|
||||
) => {
|
||||
const { data, mouseX, xScale } = props
|
||||
const [start, end] = xScale.domain()
|
||||
const d = xScale.invert(mouseX)
|
||||
return (
|
||||
<Row className="items-center gap-2">
|
||||
{datum && <Avatar size="xs" avatarUrl={datum.userAvatarUrl} />}
|
||||
<span className="font-semibold">{formatDateInRange(x, start, end)}</span>
|
||||
<span className="text-greyscale-6">{formatLargeNumber(y)}</span>
|
||||
{data.obj && <Avatar size="xs" avatarUrl={data.obj.userAvatarUrl} />}
|
||||
<span className="font-semibold">{formatDateInRange(d, start, end)}</span>
|
||||
<span className="text-greyscale-6">{formatLargeNumber(data.y)}</span>
|
||||
</Row>
|
||||
)
|
||||
}
|
||||
|
@ -57,10 +57,11 @@ const PseudoNumericChartTooltip = (props: TooltipProps<HistoryPoint<Bet>>) => {
|
|||
export const PseudoNumericContractChart = (props: {
|
||||
contract: PseudoNumericContract
|
||||
bets: Bet[]
|
||||
height?: number
|
||||
width: number
|
||||
height: number
|
||||
onMouseOver?: (p: HistoryPoint<Bet> | undefined) => void
|
||||
}) => {
|
||||
const { contract, bets, onMouseOver } = props
|
||||
const { contract, bets, width, height, onMouseOver } = props
|
||||
const { min, max, isLogScale } = contract
|
||||
const [start, end] = getDateRange(contract)
|
||||
const scaleP = useMemo(
|
||||
|
@ -84,30 +85,21 @@ export const PseudoNumericContractChart = (props: {
|
|||
Date.now()
|
||||
)
|
||||
const visibleRange = [start, rightmostDate]
|
||||
const isMobile = useIsMobile(800)
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const width = useElementWidth(containerRef) ?? 0
|
||||
const height = props.height ?? (isMobile ? 150 : 250)
|
||||
const xScale = scaleTime(visibleRange, [0, width - MARGIN_X])
|
||||
const xScale = scaleTime(visibleRange, [0, width ?? 0 - MARGIN_X])
|
||||
// clamp log scale to make sure zeroes go to the bottom
|
||||
const yScale = isLogScale
|
||||
? scaleLog([Math.max(min, 1), max], [height - MARGIN_Y, 0]).clamp(true)
|
||||
: scaleLinear([min, max], [height - MARGIN_Y, 0])
|
||||
|
||||
? scaleLog([Math.max(min, 1), max], [height ?? 0 - MARGIN_Y, 0]).clamp(true)
|
||||
: scaleLinear([min, max], [height ?? 0 - MARGIN_Y, 0])
|
||||
return (
|
||||
<div ref={containerRef}>
|
||||
{width > 0 && (
|
||||
<SingleValueHistoryChart
|
||||
w={width}
|
||||
h={height}
|
||||
xScale={xScale}
|
||||
yScale={yScale}
|
||||
data={data}
|
||||
onMouseOver={onMouseOver}
|
||||
Tooltip={PseudoNumericChartTooltip}
|
||||
color={NUMERIC_GRAPH_COLOR}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<SingleValueHistoryChart
|
||||
w={width}
|
||||
h={height}
|
||||
xScale={xScale}
|
||||
yScale={yScale}
|
||||
data={data}
|
||||
onMouseOver={onMouseOver}
|
||||
Tooltip={PseudoNumericChartTooltip}
|
||||
color={NUMERIC_GRAPH_COLOR}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ import {
|
|||
import { range } from 'lodash'
|
||||
|
||||
import {
|
||||
ContinuousScale,
|
||||
SVGChart,
|
||||
AreaPath,
|
||||
AreaWithTopStroke,
|
||||
|
@ -31,6 +32,19 @@ const getTickValues = (min: number, max: number, n: number) => {
|
|||
return [min, ...range(1, n - 1).map((i) => min + step * i), max]
|
||||
}
|
||||
|
||||
const betAtPointSelector = <X, Y, P extends Point<X, Y>>(
|
||||
data: P[],
|
||||
xScale: ContinuousScale<X>
|
||||
) => {
|
||||
const bisect = bisector((p: P) => p.x)
|
||||
return (posX: number) => {
|
||||
const x = xScale.invert(posX)
|
||||
const item = data[bisect.left(data, x) - 1]
|
||||
const result = item ? { ...item, x: posX } : undefined
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
export const DistributionChart = <P extends DistributionPoint>(props: {
|
||||
data: P[]
|
||||
w: number
|
||||
|
@ -39,7 +53,7 @@ export const DistributionChart = <P extends DistributionPoint>(props: {
|
|||
xScale: ScaleContinuousNumeric<number, number>
|
||||
yScale: ScaleContinuousNumeric<number, number>
|
||||
onMouseOver?: (p: P | undefined) => void
|
||||
Tooltip?: TooltipComponent<P>
|
||||
Tooltip?: TooltipComponent<number, P>
|
||||
}) => {
|
||||
const { color, data, yScale, w, h, Tooltip } = props
|
||||
|
||||
|
@ -50,7 +64,6 @@ export const DistributionChart = <P extends DistributionPoint>(props: {
|
|||
const px = useCallback((p: P) => xScale(p.x), [xScale])
|
||||
const py0 = yScale(yScale.domain()[0])
|
||||
const py1 = useCallback((p: P) => yScale(p.y), [yScale])
|
||||
const xBisector = bisector((p: P) => p.x)
|
||||
|
||||
const { xAxis, yAxis } = useMemo(() => {
|
||||
const xAxis = axisBottom<number>(xScale).ticks(w / 100)
|
||||
|
@ -58,6 +71,8 @@ export const DistributionChart = <P extends DistributionPoint>(props: {
|
|||
return { xAxis, yAxis }
|
||||
}, [w, xScale, yScale])
|
||||
|
||||
const onMouseOver = useEvent(betAtPointSelector(data, xScale))
|
||||
|
||||
const onSelect = useEvent((ev: D3BrushEvent<P>) => {
|
||||
if (ev.selection) {
|
||||
const [mouseX0, mouseX1] = ev.selection as [number, number]
|
||||
|
@ -69,14 +84,6 @@ export const DistributionChart = <P extends DistributionPoint>(props: {
|
|||
}
|
||||
})
|
||||
|
||||
const onMouseOver = useEvent((mouseX: number) => {
|
||||
const queryX = xScale.invert(mouseX)
|
||||
const item = data[xBisector.left(data, queryX) - 1]
|
||||
const result = item ? { ...item, x: queryX } : undefined
|
||||
props.onMouseOver?.(result)
|
||||
return result
|
||||
})
|
||||
|
||||
return (
|
||||
<SVGChart
|
||||
w={w}
|
||||
|
@ -107,7 +114,7 @@ export const MultiValueHistoryChart = <P extends MultiPoint>(props: {
|
|||
xScale: ScaleTime<number, number>
|
||||
yScale: ScaleContinuousNumeric<number, number>
|
||||
onMouseOver?: (p: P | undefined) => void
|
||||
Tooltip?: TooltipComponent<P>
|
||||
Tooltip?: TooltipComponent<Date, P>
|
||||
pct?: boolean
|
||||
}) => {
|
||||
const { colors, data, yScale, w, h, Tooltip, pct } = props
|
||||
|
@ -119,7 +126,6 @@ export const MultiValueHistoryChart = <P extends MultiPoint>(props: {
|
|||
const px = useCallback((p: SP) => xScale(p.data.x), [xScale])
|
||||
const py0 = useCallback((p: SP) => yScale(p[0]), [yScale])
|
||||
const py1 = useCallback((p: SP) => yScale(p[1]), [yScale])
|
||||
const xBisector = bisector((p: P) => p.x)
|
||||
|
||||
const { xAxis, yAxis } = useMemo(() => {
|
||||
const [min, max] = yScale.domain()
|
||||
|
@ -141,6 +147,8 @@ export const MultiValueHistoryChart = <P extends MultiPoint>(props: {
|
|||
return d3Stack(data)
|
||||
}, [data])
|
||||
|
||||
const onMouseOver = useEvent(betAtPointSelector(data, xScale))
|
||||
|
||||
const onSelect = useEvent((ev: D3BrushEvent<P>) => {
|
||||
if (ev.selection) {
|
||||
const [mouseX0, mouseX1] = ev.selection as [number, number]
|
||||
|
@ -152,14 +160,6 @@ export const MultiValueHistoryChart = <P extends MultiPoint>(props: {
|
|||
}
|
||||
})
|
||||
|
||||
const onMouseOver = useEvent((mouseX: number) => {
|
||||
const queryX = xScale.invert(mouseX)
|
||||
const item = data[xBisector.left(data, queryX) - 1]
|
||||
const result = item ? { ...item, x: queryX } : undefined
|
||||
props.onMouseOver?.(result)
|
||||
return result
|
||||
})
|
||||
|
||||
return (
|
||||
<SVGChart
|
||||
w={w}
|
||||
|
@ -193,7 +193,7 @@ export const SingleValueHistoryChart = <P extends HistoryPoint>(props: {
|
|||
xScale: ScaleTime<number, number>
|
||||
yScale: ScaleContinuousNumeric<number, number>
|
||||
onMouseOver?: (p: P | undefined) => void
|
||||
Tooltip?: TooltipComponent<P>
|
||||
Tooltip?: TooltipComponent<Date, P>
|
||||
pct?: boolean
|
||||
}) => {
|
||||
const { color, data, yScale, w, h, Tooltip, pct } = props
|
||||
|
@ -204,7 +204,6 @@ export const SingleValueHistoryChart = <P extends HistoryPoint>(props: {
|
|||
const px = useCallback((p: P) => xScale(p.x), [xScale])
|
||||
const py0 = yScale(yScale.domain()[0])
|
||||
const py1 = useCallback((p: P) => yScale(p.y), [yScale])
|
||||
const xBisector = bisector((p: P) => p.x)
|
||||
|
||||
const { xAxis, yAxis } = useMemo(() => {
|
||||
const [min, max] = yScale.domain()
|
||||
|
@ -218,6 +217,8 @@ export const SingleValueHistoryChart = <P extends HistoryPoint>(props: {
|
|||
return { xAxis, yAxis }
|
||||
}, [w, h, pct, xScale, yScale])
|
||||
|
||||
const onMouseOver = useEvent(betAtPointSelector(data, xScale))
|
||||
|
||||
const onSelect = useEvent((ev: D3BrushEvent<P>) => {
|
||||
if (ev.selection) {
|
||||
const [mouseX0, mouseX1] = ev.selection as [number, number]
|
||||
|
@ -229,14 +230,6 @@ export const SingleValueHistoryChart = <P extends HistoryPoint>(props: {
|
|||
}
|
||||
})
|
||||
|
||||
const onMouseOver = useEvent((mouseX: number) => {
|
||||
const queryX = xScale.invert(mouseX)
|
||||
const item = data[xBisector.left(data, queryX) - 1]
|
||||
const result = item ? { ...item, x: queryX } : undefined
|
||||
props.onMouseOver?.(result)
|
||||
return result
|
||||
})
|
||||
|
||||
return (
|
||||
<SVGChart
|
||||
w={w}
|
||||
|
|
|
@ -16,8 +16,14 @@ import dayjs from 'dayjs'
|
|||
import clsx from 'clsx'
|
||||
|
||||
import { Contract } from 'common/contract'
|
||||
import { useMeasureSize } from 'web/hooks/use-measure-size'
|
||||
|
||||
export type Point<X, Y, T = unknown> = { x: X; y: Y; obj?: T }
|
||||
|
||||
export interface ContinuousScale<T> extends AxisScale<T> {
|
||||
invert(n: number): T
|
||||
}
|
||||
|
||||
export type Point<X, Y, T = unknown> = { x: X; y: Y; datum?: T }
|
||||
export type XScale<P> = P extends Point<infer X, infer _> ? AxisScale<X> : never
|
||||
export type YScale<P> = P extends Point<infer _, infer Y> ? AxisScale<Y> : never
|
||||
|
||||
|
@ -118,18 +124,19 @@ export const AreaWithTopStroke = <P,>(props: {
|
|||
)
|
||||
}
|
||||
|
||||
export const SVGChart = <X, Y, P extends Point<X, Y>>(props: {
|
||||
export const SVGChart = <X, TT>(props: {
|
||||
children: ReactNode
|
||||
w: number
|
||||
h: number
|
||||
xAxis: Axis<X>
|
||||
yAxis: Axis<number>
|
||||
onSelect?: (ev: D3BrushEvent<any>) => void
|
||||
onMouseOver?: (mouseX: number, mouseY: number) => P | undefined
|
||||
Tooltip?: TooltipComponent<P>
|
||||
onMouseOver?: (mouseX: number, mouseY: number) => TT | undefined
|
||||
Tooltip?: TooltipComponent<X, TT>
|
||||
}) => {
|
||||
const { children, w, h, xAxis, yAxis, onMouseOver, onSelect, Tooltip } = props
|
||||
const [mouseState, setMouseState] = useState<{ pos: TooltipPosition; p: P }>()
|
||||
const [mouse, setMouse] = useState<{ x: number; y: number; data: TT }>()
|
||||
const tooltipMeasure = useMeasureSize()
|
||||
const overlayRef = useRef<SVGGElement>(null)
|
||||
const innerW = w - MARGIN_X
|
||||
const innerH = h - MARGIN_Y
|
||||
|
@ -148,7 +155,7 @@ export const SVGChart = <X, Y, P extends Point<X, Y>>(props: {
|
|||
if (!justSelected.current) {
|
||||
justSelected.current = true
|
||||
onSelect(ev)
|
||||
setMouseState(undefined)
|
||||
setMouse(undefined)
|
||||
if (overlayRef.current) {
|
||||
select(overlayRef.current).call(brush.clear)
|
||||
}
|
||||
|
@ -168,26 +175,40 @@ export const SVGChart = <X, Y, P extends Point<X, Y>>(props: {
|
|||
|
||||
const onPointerMove = (ev: React.PointerEvent) => {
|
||||
if (ev.pointerType === 'mouse' && onMouseOver) {
|
||||
const [mouseX, mouseY] = pointer(ev)
|
||||
const p = onMouseOver(mouseX, mouseY)
|
||||
if (p != null) {
|
||||
const pos = getTooltipPosition(mouseX, mouseY, innerW, innerH)
|
||||
setMouseState({ pos, p })
|
||||
const [x, y] = pointer(ev)
|
||||
const data = onMouseOver(x, y)
|
||||
if (data !== undefined) {
|
||||
setMouse({ x, y, data })
|
||||
} else {
|
||||
setMouseState(undefined)
|
||||
setMouse(undefined)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const onPointerLeave = () => {
|
||||
setMouseState(undefined)
|
||||
setMouse(undefined)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
{mouseState && Tooltip && (
|
||||
<TooltipContainer pos={mouseState.pos}>
|
||||
<Tooltip xScale={xAxis.scale()} p={mouseState.p} />
|
||||
<div className="relative overflow-hidden">
|
||||
{mouse && Tooltip && (
|
||||
<TooltipContainer
|
||||
setElem={tooltipMeasure.setElem}
|
||||
pos={getTooltipPosition(
|
||||
mouse.x,
|
||||
mouse.y,
|
||||
innerW,
|
||||
innerH,
|
||||
tooltipMeasure.width,
|
||||
tooltipMeasure.height
|
||||
)}
|
||||
>
|
||||
<Tooltip
|
||||
xScale={xAxis.scale()}
|
||||
mouseX={mouse.x}
|
||||
mouseY={mouse.y}
|
||||
data={mouse.data}
|
||||
/>
|
||||
</TooltipContainer>
|
||||
)}
|
||||
<svg width={w} height={h} viewBox={`0 0 ${w} ${h}`}>
|
||||
|
@ -216,43 +237,51 @@ export const SVGChart = <X, Y, P extends Point<X, Y>>(props: {
|
|||
)
|
||||
}
|
||||
|
||||
export type TooltipPosition = {
|
||||
top?: number
|
||||
right?: number
|
||||
bottom?: number
|
||||
left?: number
|
||||
}
|
||||
export type TooltipPosition = { left: number; bottom: number }
|
||||
|
||||
export const getTooltipPosition = (
|
||||
mouseX: number,
|
||||
mouseY: number,
|
||||
w: number,
|
||||
h: number
|
||||
containerWidth: number,
|
||||
containerHeight: number,
|
||||
tooltipWidth?: number,
|
||||
tooltipHeight?: number
|
||||
) => {
|
||||
const result: TooltipPosition = {}
|
||||
if (mouseX <= (3 * w) / 4) {
|
||||
result.left = mouseX + 10 // in the left three quarters
|
||||
} else {
|
||||
result.right = w - mouseX + 10 // in the right quarter
|
||||
let left = mouseX + 12
|
||||
let bottom = containerHeight - mouseY + 12
|
||||
if (tooltipWidth != null) {
|
||||
const overflow = left + tooltipWidth - containerWidth
|
||||
if (overflow > 0) {
|
||||
left -= overflow
|
||||
}
|
||||
}
|
||||
if (mouseY <= h / 4) {
|
||||
result.top = mouseY + 10 // in the top quarter
|
||||
} else {
|
||||
result.bottom = h - mouseY + 10 // in the bottom three quarters
|
||||
if (tooltipHeight != null) {
|
||||
const overflow = tooltipHeight - mouseY
|
||||
if (overflow > 0) {
|
||||
bottom -= overflow
|
||||
}
|
||||
}
|
||||
return result
|
||||
return { left, bottom }
|
||||
}
|
||||
|
||||
export type TooltipProps<P> = { p: P; xScale: XScale<P> }
|
||||
export type TooltipComponent<P> = React.ComponentType<TooltipProps<P>>
|
||||
export type TooltipProps<X, T> = {
|
||||
mouseX: number
|
||||
mouseY: number
|
||||
xScale: ContinuousScale<X>
|
||||
data: T
|
||||
}
|
||||
|
||||
export type TooltipComponent<X, T> = React.ComponentType<TooltipProps<X, T>>
|
||||
export const TooltipContainer = (props: {
|
||||
setElem: (e: HTMLElement | null) => void
|
||||
pos: TooltipPosition
|
||||
className?: string
|
||||
children: React.ReactNode
|
||||
}) => {
|
||||
const { pos, className, children } = props
|
||||
const { setElem, pos, className, children } = props
|
||||
return (
|
||||
<div
|
||||
ref={setElem}
|
||||
className={clsx(
|
||||
className,
|
||||
'pointer-events-none absolute z-10 whitespace-pre rounded border border-gray-200 bg-white/80 p-2 px-4 py-2 text-xs sm:text-sm'
|
||||
|
|
|
@ -22,7 +22,10 @@ export function BountiedContractSmallBadge(props: {
|
|||
|
||||
return (
|
||||
<Tooltip
|
||||
text={CommentBountiesTooltipText(openCommentBounties)}
|
||||
text={CommentBountiesTooltipText(
|
||||
contract.creatorName,
|
||||
openCommentBounties
|
||||
)}
|
||||
placement="bottom"
|
||||
>
|
||||
<span className="inline-flex items-center gap-1 whitespace-nowrap rounded-full bg-indigo-300 px-2 py-0.5 text-xs font-medium text-white">
|
||||
|
@ -33,8 +36,11 @@ export function BountiedContractSmallBadge(props: {
|
|||
)
|
||||
}
|
||||
|
||||
export const CommentBountiesTooltipText = (openCommentBounties: number) =>
|
||||
`The creator of this market may award ${formatMoney(
|
||||
export const CommentBountiesTooltipText = (
|
||||
creator: string,
|
||||
openCommentBounties: number
|
||||
) =>
|
||||
`${creator} may award ${formatMoney(
|
||||
COMMENT_BOUNTY_AMOUNT
|
||||
)} for good comments. ${formatMoney(
|
||||
openCommentBounties
|
||||
|
|
|
@ -183,6 +183,7 @@ export function MarketSubheader(props: {
|
|||
contract={contract}
|
||||
resolvedDate={resolvedDate}
|
||||
isCreator={isCreator}
|
||||
disabled={disabled}
|
||||
/>
|
||||
{!isMobile && (
|
||||
<Row className={'gap-1'}>
|
||||
|
@ -200,8 +201,9 @@ export function CloseOrResolveTime(props: {
|
|||
contract: Contract
|
||||
resolvedDate: any
|
||||
isCreator: boolean
|
||||
disabled?: boolean
|
||||
}) {
|
||||
const { contract, resolvedDate, isCreator } = props
|
||||
const { contract, resolvedDate, isCreator, disabled } = props
|
||||
const { resolutionTime, closeTime } = contract
|
||||
if (!!closeTime || !!resolvedDate) {
|
||||
return (
|
||||
|
@ -225,6 +227,7 @@ export function CloseOrResolveTime(props: {
|
|||
closeTime={closeTime}
|
||||
contract={contract}
|
||||
isCreator={isCreator ?? false}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</Row>
|
||||
)}
|
||||
|
@ -245,7 +248,8 @@ export function MarketGroups(props: {
|
|||
return (
|
||||
<>
|
||||
<Row className="items-center gap-1">
|
||||
<GroupDisplay groupToDisplay={groupToDisplay} />
|
||||
<GroupDisplay groupToDisplay={groupToDisplay} disabled={disabled} />
|
||||
|
||||
{!disabled && user && (
|
||||
<button
|
||||
className="text-greyscale-4 hover:text-greyscale-3"
|
||||
|
@ -330,14 +334,29 @@ export function ExtraMobileContractDetails(props: {
|
|||
)
|
||||
}
|
||||
|
||||
export function GroupDisplay(props: { groupToDisplay?: GroupLink | null }) {
|
||||
const { groupToDisplay } = props
|
||||
export function GroupDisplay(props: {
|
||||
groupToDisplay?: GroupLink | null
|
||||
disabled?: boolean
|
||||
}) {
|
||||
const { groupToDisplay, disabled } = props
|
||||
|
||||
if (groupToDisplay) {
|
||||
return (
|
||||
const groupSection = (
|
||||
<a
|
||||
className={clsx(
|
||||
'bg-greyscale-4 max-w-[140px] truncate whitespace-nowrap rounded-full py-0.5 px-2 text-xs text-white sm:max-w-[250px]',
|
||||
!disabled && 'hover:bg-greyscale-3 cursor-pointer'
|
||||
)}
|
||||
>
|
||||
{groupToDisplay.name}
|
||||
</a>
|
||||
)
|
||||
|
||||
return disabled ? (
|
||||
groupSection
|
||||
) : (
|
||||
<Link prefetch={false} href={groupPath(groupToDisplay.slug)}>
|
||||
<a className="bg-greyscale-4 hover:bg-greyscale-3 max-w-[140px] truncate whitespace-nowrap rounded-full py-0.5 px-2 text-xs text-white sm:max-w-[250px]">
|
||||
{groupToDisplay.name}
|
||||
</a>
|
||||
{groupSection}
|
||||
</Link>
|
||||
)
|
||||
} else
|
||||
|
@ -352,8 +371,9 @@ function EditableCloseDate(props: {
|
|||
closeTime: number
|
||||
contract: Contract
|
||||
isCreator: boolean
|
||||
disabled?: boolean
|
||||
}) {
|
||||
const { closeTime, contract, isCreator } = props
|
||||
const { closeTime, contract, isCreator, disabled } = props
|
||||
|
||||
const dayJsCloseTime = dayjs(closeTime)
|
||||
const dayJsNow = dayjs()
|
||||
|
@ -452,8 +472,8 @@ function EditableCloseDate(props: {
|
|||
time={closeTime}
|
||||
>
|
||||
<span
|
||||
className={isCreator ? 'cursor-pointer' : ''}
|
||||
onClick={() => isCreator && setIsEditingCloseTime(true)}
|
||||
className={!disabled && isCreator ? 'cursor-pointer' : ''}
|
||||
onClick={() => !disabled && isCreator && setIsEditingCloseTime(true)}
|
||||
>
|
||||
{isSameDay ? (
|
||||
<span className={'capitalize'}> {fromNow(closeTime)}</span>
|
||||
|
|
|
@ -1,13 +1,8 @@
|
|||
import React from 'react'
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
|
||||
import { tradingAllowed } from 'web/lib/firebase/contracts'
|
||||
import { Col } from '../layout/col'
|
||||
import {
|
||||
BinaryContractChart,
|
||||
NumericContractChart,
|
||||
PseudoNumericContractChart,
|
||||
ChoiceContractChart,
|
||||
} from 'web/components/charts/contract'
|
||||
import { ContractChart } from 'web/components/charts/contract'
|
||||
import { useUser } from 'web/hooks/use-user'
|
||||
import { Row } from '../layout/row'
|
||||
import { Linkify } from '../linkify'
|
||||
|
@ -48,8 +43,43 @@ const BetWidget = (props: { contract: CPMMContract }) => {
|
|||
)
|
||||
}
|
||||
|
||||
const NumericOverview = (props: { contract: NumericContract }) => {
|
||||
const { contract } = props
|
||||
const SizedContractChart = (props: {
|
||||
contract: Contract
|
||||
bets: Bet[]
|
||||
fullHeight: number
|
||||
mobileHeight: number
|
||||
}) => {
|
||||
const { contract, bets, fullHeight, mobileHeight } = props
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const [chartWidth, setChartWidth] = useState<number>()
|
||||
const [chartHeight, setChartHeight] = useState<number>()
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
setChartHeight(window.innerWidth < 800 ? mobileHeight : fullHeight)
|
||||
setChartWidth(containerRef.current?.clientWidth)
|
||||
}
|
||||
handleResize()
|
||||
window.addEventListener('resize', handleResize)
|
||||
return () => {
|
||||
window.removeEventListener('resize', handleResize)
|
||||
}
|
||||
}, [fullHeight, mobileHeight])
|
||||
return (
|
||||
<div ref={containerRef}>
|
||||
{chartWidth != null && chartHeight != null && (
|
||||
<ContractChart
|
||||
contract={contract}
|
||||
bets={bets}
|
||||
width={chartWidth}
|
||||
height={chartHeight}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const NumericOverview = (props: { contract: NumericContract; bets: Bet[] }) => {
|
||||
const { contract, bets } = props
|
||||
return (
|
||||
<Col className="gap-1 md:gap-2">
|
||||
<Col className="gap-3 px-2 sm:gap-4">
|
||||
|
@ -66,7 +96,12 @@ const NumericOverview = (props: { contract: NumericContract }) => {
|
|||
contract={contract}
|
||||
/>
|
||||
</Col>
|
||||
<NumericContractChart contract={contract} />
|
||||
<SizedContractChart
|
||||
contract={contract}
|
||||
bets={bets}
|
||||
fullHeight={250}
|
||||
mobileHeight={150}
|
||||
/>
|
||||
</Col>
|
||||
)
|
||||
}
|
||||
|
@ -82,7 +117,12 @@ const BinaryOverview = (props: { contract: BinaryContract; bets: Bet[] }) => {
|
|||
<BinaryResolutionOrChance contract={contract} large />
|
||||
</Row>
|
||||
</Col>
|
||||
<BinaryContractChart contract={contract} bets={bets} />
|
||||
<SizedContractChart
|
||||
contract={contract}
|
||||
bets={bets}
|
||||
fullHeight={250}
|
||||
mobileHeight={150}
|
||||
/>
|
||||
<Row className="items-center justify-between gap-4 xl:hidden">
|
||||
{tradingAllowed(contract) && (
|
||||
<BinaryMobileBetting contract={contract} />
|
||||
|
@ -107,9 +147,12 @@ const ChoiceOverview = (props: {
|
|||
<FreeResponseResolutionOrChance contract={contract} truncate="none" />
|
||||
)}
|
||||
</Col>
|
||||
<Col className={'mb-1 gap-y-2'}>
|
||||
<ChoiceContractChart contract={contract} bets={bets} />
|
||||
</Col>
|
||||
<SizedContractChart
|
||||
contract={contract}
|
||||
bets={bets}
|
||||
fullHeight={350}
|
||||
mobileHeight={250}
|
||||
/>
|
||||
</Col>
|
||||
)
|
||||
}
|
||||
|
@ -135,7 +178,12 @@ const PseudoNumericOverview = (props: {
|
|||
{tradingAllowed(contract) && <BetWidget contract={contract} />}
|
||||
</Row>
|
||||
</Col>
|
||||
<PseudoNumericContractChart contract={contract} bets={bets} />
|
||||
<SizedContractChart
|
||||
contract={contract}
|
||||
bets={bets}
|
||||
fullHeight={250}
|
||||
mobileHeight={150}
|
||||
/>
|
||||
</Col>
|
||||
)
|
||||
}
|
||||
|
@ -149,7 +197,7 @@ export const ContractOverview = (props: {
|
|||
case 'BINARY':
|
||||
return <BinaryOverview contract={contract} bets={bets} />
|
||||
case 'NUMERIC':
|
||||
return <NumericOverview contract={contract} />
|
||||
return <NumericOverview contract={contract} bets={bets} />
|
||||
case 'PSEUDO_NUMERIC':
|
||||
return <PseudoNumericOverview contract={contract} bets={bets} />
|
||||
case 'FREE_RESPONSE':
|
||||
|
|
|
@ -75,7 +75,7 @@ const CommentsTabContent = memo(function CommentsTabContent(props: {
|
|||
const { contract } = props
|
||||
const tips = useTipTxns({ contractId: contract.id })
|
||||
const comments = useComments(contract.id) ?? props.comments
|
||||
const [sort, setSort] = useState<'Newest' | 'Best'>('Best')
|
||||
const [sort, setSort] = useState<'Newest' | 'Best'>('Newest')
|
||||
const me = useUser()
|
||||
if (comments == null) {
|
||||
return <LoadingIndicator />
|
||||
|
@ -159,7 +159,7 @@ const CommentsTabContent = memo(function CommentsTabContent(props: {
|
|||
<Tooltip
|
||||
text={
|
||||
sort === 'Best'
|
||||
? 'Comments with tips or bounties will be shown first. Your comments made within the last 10 minutes will temporarily appear (to you) first.'
|
||||
? 'Highest tips + bounties first. Your new comments briefly appear to you first.'
|
||||
: ''
|
||||
}
|
||||
>
|
||||
|
|
|
@ -18,9 +18,7 @@ export function ExtraContractActionsRow(props: { contract: Contract }) {
|
|||
return (
|
||||
<Row>
|
||||
<FollowMarketButton contract={contract} user={user} />
|
||||
{user?.id !== contract.creatorId && (
|
||||
<LikeMarketButton contract={contract} user={user} />
|
||||
)}
|
||||
<LikeMarketButton contract={contract} user={user} />
|
||||
<Tooltip text="Share" placement="bottom" noTap noFade>
|
||||
<Button
|
||||
size="sm"
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
import { HeartIcon } from '@heroicons/react/outline'
|
||||
import { Button } from 'web/components/button'
|
||||
import React, { useMemo } from 'react'
|
||||
import React, { useMemo, useState } from 'react'
|
||||
import { Contract } from 'common/contract'
|
||||
import { User } from 'common/user'
|
||||
import { useUserLikes } from 'web/hooks/use-likes'
|
||||
|
@ -8,74 +6,51 @@ import toast from 'react-hot-toast'
|
|||
import { formatMoney } from 'common/util/format'
|
||||
import { likeContract } from 'web/lib/firebase/likes'
|
||||
import { LIKE_TIP_AMOUNT } from 'common/like'
|
||||
import clsx from 'clsx'
|
||||
import { Col } from 'web/components/layout/col'
|
||||
import { firebaseLogin } from 'web/lib/firebase/users'
|
||||
import { useMarketTipTxns } from 'web/hooks/use-tip-txns'
|
||||
import { sum } from 'lodash'
|
||||
import { Tooltip } from '../tooltip'
|
||||
import { TipButton } from './tip-button'
|
||||
|
||||
export function LikeMarketButton(props: {
|
||||
contract: Contract
|
||||
user: User | null | undefined
|
||||
}) {
|
||||
const { contract, user } = props
|
||||
const tips = useMarketTipTxns(contract.id).filter(
|
||||
(txn) => txn.fromId === user?.id
|
||||
)
|
||||
|
||||
const tips = useMarketTipTxns(contract.id)
|
||||
|
||||
const totalTipped = useMemo(() => {
|
||||
return sum(tips.map((tip) => tip.amount))
|
||||
}, [tips])
|
||||
|
||||
const likes = useUserLikes(user?.id)
|
||||
|
||||
const [isLiking, setIsLiking] = useState(false)
|
||||
|
||||
const userLikedContractIds = likes
|
||||
?.filter((l) => l.type === 'contract')
|
||||
.map((l) => l.id)
|
||||
|
||||
const onLike = async () => {
|
||||
if (!user) return firebaseLogin()
|
||||
await likeContract(user, contract)
|
||||
|
||||
setIsLiking(true)
|
||||
likeContract(user, contract).catch(() => setIsLiking(false))
|
||||
toast(`You tipped ${contract.creatorName} ${formatMoney(LIKE_TIP_AMOUNT)}!`)
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
text={`Tip ${formatMoney(LIKE_TIP_AMOUNT)}`}
|
||||
placement="bottom"
|
||||
noTap
|
||||
noFade
|
||||
>
|
||||
<Button
|
||||
size={'sm'}
|
||||
className={'max-w-xs self-center'}
|
||||
color={'gray-white'}
|
||||
onClick={onLike}
|
||||
>
|
||||
<Col className={'relative items-center sm:flex-row'}>
|
||||
<HeartIcon
|
||||
className={clsx(
|
||||
'h-5 w-5 sm:h-6 sm:w-6',
|
||||
totalTipped > 0 ? 'mr-2' : '',
|
||||
user &&
|
||||
(userLikedContractIds?.includes(contract.id) ||
|
||||
(!likes && contract.likedByUserIds?.includes(user.id)))
|
||||
? 'fill-red-500 text-red-500'
|
||||
: ''
|
||||
)}
|
||||
/>
|
||||
{totalTipped > 0 && (
|
||||
<div
|
||||
className={clsx(
|
||||
'bg-greyscale-6 absolute ml-3.5 mt-2 h-4 w-4 rounded-full align-middle text-white sm:mt-3 sm:h-5 sm:w-5 sm:px-1',
|
||||
totalTipped > 99
|
||||
? 'text-[0.4rem] sm:text-[0.5rem]'
|
||||
: 'sm:text-2xs text-[0.5rem]'
|
||||
)}
|
||||
>
|
||||
{totalTipped}
|
||||
</div>
|
||||
)}
|
||||
</Col>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<TipButton
|
||||
onClick={onLike}
|
||||
tipAmount={LIKE_TIP_AMOUNT}
|
||||
totalTipped={totalTipped}
|
||||
userTipped={
|
||||
!!user &&
|
||||
(isLiking ||
|
||||
userLikedContractIds?.includes(contract.id) ||
|
||||
(!likes && !!contract.likedByUserIds?.includes(user.id)))
|
||||
}
|
||||
disabled={contract.creatorId === user?.id}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
61
web/components/contract/tip-button.tsx
Normal file
61
web/components/contract/tip-button.tsx
Normal file
|
@ -0,0 +1,61 @@
|
|||
import { HeartIcon } from '@heroicons/react/outline'
|
||||
import { Button } from 'web/components/button'
|
||||
import { formatMoney } from 'common/util/format'
|
||||
import clsx from 'clsx'
|
||||
import { Col } from 'web/components/layout/col'
|
||||
import { Tooltip } from '../tooltip'
|
||||
|
||||
export function TipButton(props: {
|
||||
tipAmount: number
|
||||
totalTipped: number
|
||||
onClick: () => void
|
||||
userTipped: boolean
|
||||
isCompact?: boolean
|
||||
disabled?: boolean
|
||||
}) {
|
||||
const { tipAmount, totalTipped, userTipped, isCompact, onClick, disabled } =
|
||||
props
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
text={disabled ? 'Tips' : `Tip ${formatMoney(tipAmount)}`}
|
||||
placement="bottom"
|
||||
noTap
|
||||
noFade
|
||||
>
|
||||
<Button
|
||||
size={'sm'}
|
||||
className={clsx(
|
||||
'max-w-xs self-center',
|
||||
isCompact && 'px-0 py-0',
|
||||
disabled && 'hover:bg-inherit'
|
||||
)}
|
||||
color={'gray-white'}
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
>
|
||||
<Col className={'relative items-center sm:flex-row'}>
|
||||
<HeartIcon
|
||||
className={clsx(
|
||||
'h-5 w-5 sm:h-6 sm:w-6',
|
||||
totalTipped > 0 ? 'mr-2' : '',
|
||||
userTipped ? 'fill-green-700 text-green-700' : ''
|
||||
)}
|
||||
/>
|
||||
{totalTipped > 0 && (
|
||||
<div
|
||||
className={clsx(
|
||||
'bg-greyscale-5 absolute ml-3.5 mt-2 h-4 w-4 rounded-full align-middle text-white sm:mt-3 sm:h-5 sm:w-5 sm:px-1',
|
||||
totalTipped > 99
|
||||
? 'text-[0.4rem] sm:text-[0.5rem]'
|
||||
: 'sm:text-2xs text-[0.5rem]'
|
||||
)}
|
||||
>
|
||||
{totalTipped}
|
||||
</div>
|
||||
)}
|
||||
</Col>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
|
@ -177,10 +177,6 @@ export function FeedComment(props: {
|
|||
smallImage
|
||||
/>
|
||||
<Row className="mt-2 items-center gap-6 text-xs text-gray-500">
|
||||
{tips && <Tipper comment={comment} tips={tips} />}
|
||||
{(contract.openCommentBounties ?? 0) > 0 && (
|
||||
<AwardBountyButton comment={comment} contract={contract} />
|
||||
)}
|
||||
{onReplyClick && (
|
||||
<button
|
||||
className="font-bold hover:underline"
|
||||
|
@ -189,6 +185,10 @@ export function FeedComment(props: {
|
|||
Reply
|
||||
</button>
|
||||
)}
|
||||
{tips && <Tipper comment={comment} tips={tips} />}
|
||||
{(contract.openCommentBounties ?? 0) > 0 && (
|
||||
<AwardBountyButton comment={comment} contract={contract} />
|
||||
)}
|
||||
</Row>
|
||||
</div>
|
||||
</Row>
|
||||
|
|
|
@ -32,27 +32,27 @@ export function GroupSelector(props: {
|
|||
const openGroups = useOpenGroups()
|
||||
const memberGroups = useMemberGroups(creator?.id)
|
||||
const memberGroupIds = memberGroups?.map((g) => g.id) ?? []
|
||||
const availableGroups = openGroups
|
||||
.concat(
|
||||
(memberGroups ?? []).filter(
|
||||
(g) => !openGroups.map((og) => og.id).includes(g.id)
|
||||
)
|
||||
)
|
||||
.filter((group) => !ignoreGroupIds?.includes(group.id))
|
||||
.sort((a, b) => b.totalContracts - a.totalContracts)
|
||||
// put the groups the user is a member of first
|
||||
.sort((a, b) => {
|
||||
if (memberGroupIds.includes(a.id)) {
|
||||
return -1
|
||||
}
|
||||
if (memberGroupIds.includes(b.id)) {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
})
|
||||
|
||||
const filteredGroups = availableGroups.filter((group) =>
|
||||
searchInAny(query, group.name)
|
||||
const sortGroups = (groups: Group[]) =>
|
||||
groups.sort(
|
||||
(a, b) =>
|
||||
// weight group higher if user is a member
|
||||
(memberGroupIds.includes(b.id) ? 5 : 1) * b.totalContracts -
|
||||
(memberGroupIds.includes(a.id) ? 5 : 1) * a.totalContracts
|
||||
)
|
||||
|
||||
const availableGroups = sortGroups(
|
||||
openGroups
|
||||
.concat(
|
||||
(memberGroups ?? []).filter(
|
||||
(g) => !openGroups.map((og) => og.id).includes(g.id)
|
||||
)
|
||||
)
|
||||
.filter((group) => !ignoreGroupIds?.includes(group.id))
|
||||
)
|
||||
|
||||
const filteredGroups = sortGroups(
|
||||
availableGroups.filter((group) => searchInAny(query, group.name))
|
||||
)
|
||||
|
||||
if (!showSelector || !creator) {
|
||||
|
|
|
@ -164,7 +164,6 @@ function getMoreDesktopNavigation(user?: User | null) {
|
|||
{ name: 'Charity', href: '/charity' },
|
||||
{ name: 'Send M$', href: '/links' },
|
||||
{ name: 'Discord', href: 'https://discord.gg/eHQBNBqXuh' },
|
||||
{ name: 'Dating docs', href: '/date-docs' },
|
||||
{ name: 'Help & About', href: 'https://help.manifold.markets/' },
|
||||
{
|
||||
name: 'Sign out',
|
||||
|
@ -227,7 +226,6 @@ function getMoreMobileNav() {
|
|||
{ name: 'Charity', href: '/charity' },
|
||||
{ name: 'Send M$', href: '/links' },
|
||||
{ name: 'Discord', href: 'https://discord.gg/eHQBNBqXuh' },
|
||||
{ name: 'Dating docs', href: '/date-docs' },
|
||||
],
|
||||
signOut
|
||||
)
|
||||
|
|
|
@ -1,22 +1,17 @@
|
|||
import {
|
||||
ChevronDoubleRightIcon,
|
||||
ChevronLeftIcon,
|
||||
ChevronRightIcon,
|
||||
} from '@heroicons/react/solid'
|
||||
import clsx from 'clsx'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import toast from 'react-hot-toast'
|
||||
import { debounce, sum } from 'lodash'
|
||||
|
||||
import { Comment } from 'common/comment'
|
||||
import { User } from 'common/user'
|
||||
import { formatMoney } from 'common/util/format'
|
||||
import { debounce, sum } from 'lodash'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { CommentTips } from 'web/hooks/use-tip-txns'
|
||||
import { useUser } from 'web/hooks/use-user'
|
||||
import { transact } from 'web/lib/firebase/api'
|
||||
import { track } from 'web/lib/service/analytics'
|
||||
import { TipButton } from './contract/tip-button'
|
||||
import { Row } from './layout/row'
|
||||
import { Tooltip } from './tooltip'
|
||||
|
||||
const TIP_SIZE = 10
|
||||
import { LIKE_TIP_AMOUNT } from 'common/like'
|
||||
import { formatMoney } from 'common/util/format'
|
||||
|
||||
export function Tipper(prop: { comment: Comment; tips: CommentTips }) {
|
||||
const { comment, tips } = prop
|
||||
|
@ -26,6 +21,7 @@ export function Tipper(prop: { comment: Comment; tips: CommentTips }) {
|
|||
const savedTip = tips[myId] ?? 0
|
||||
|
||||
const [localTip, setLocalTip] = useState(savedTip)
|
||||
|
||||
// listen for user being set
|
||||
const initialized = useRef(false)
|
||||
useEffect(() => {
|
||||
|
@ -78,71 +74,22 @@ export function Tipper(prop: { comment: Comment; tips: CommentTips }) {
|
|||
const addTip = (delta: number) => {
|
||||
setLocalTip(localTip + delta)
|
||||
me && saveTip(me, comment, localTip - savedTip + delta)
|
||||
toast(`You tipped ${comment.userName} ${formatMoney(LIKE_TIP_AMOUNT)}!`)
|
||||
}
|
||||
|
||||
const canDown = me && localTip > savedTip
|
||||
const canUp = me && me.id !== comment.userId && me.balance >= localTip + 5
|
||||
const canUp =
|
||||
me && comment.userId !== me.id && me.balance >= localTip + LIKE_TIP_AMOUNT
|
||||
|
||||
return (
|
||||
<Row className="items-center gap-0.5">
|
||||
<DownTip onClick={canDown ? () => addTip(-TIP_SIZE) : undefined} />
|
||||
<span className="font-bold">{Math.floor(total)}</span>
|
||||
<UpTip
|
||||
onClick={canUp ? () => addTip(+TIP_SIZE) : undefined}
|
||||
value={localTip}
|
||||
<TipButton
|
||||
tipAmount={LIKE_TIP_AMOUNT}
|
||||
totalTipped={total}
|
||||
onClick={() => addTip(+LIKE_TIP_AMOUNT)}
|
||||
userTipped={localTip > 0}
|
||||
disabled={!canUp}
|
||||
isCompact
|
||||
/>
|
||||
{localTip === 0 ? (
|
||||
''
|
||||
) : (
|
||||
<span
|
||||
className={clsx(
|
||||
'ml-1 font-semibold',
|
||||
localTip > 0 ? 'text-primary' : 'text-red-400'
|
||||
)}
|
||||
>
|
||||
({formatMoney(localTip)} tip)
|
||||
</span>
|
||||
)}
|
||||
</Row>
|
||||
)
|
||||
}
|
||||
|
||||
function DownTip(props: { onClick?: () => void }) {
|
||||
const { onClick } = props
|
||||
return (
|
||||
<Tooltip
|
||||
className="h-6 w-6"
|
||||
placement="bottom"
|
||||
text={onClick && `-${formatMoney(TIP_SIZE)}`}
|
||||
noTap
|
||||
>
|
||||
<button
|
||||
className="hover:text-red-600 disabled:text-gray-100"
|
||||
disabled={!onClick}
|
||||
onClick={onClick}
|
||||
>
|
||||
<ChevronLeftIcon className="h-6 w-6" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
function UpTip(props: { onClick?: () => void; value: number }) {
|
||||
const { onClick, value } = props
|
||||
const IconKind = value > TIP_SIZE ? ChevronDoubleRightIcon : ChevronRightIcon
|
||||
return (
|
||||
<Tooltip
|
||||
className="h-6 w-6"
|
||||
placement="bottom"
|
||||
text={onClick && `Tip ${formatMoney(TIP_SIZE)}`}
|
||||
noTap
|
||||
>
|
||||
<button
|
||||
className="hover:text-primary disabled:text-gray-100"
|
||||
disabled={!onClick}
|
||||
onClick={onClick}
|
||||
>
|
||||
<IconKind className={clsx('h-6 w-6', value ? 'text-primary' : '')} />
|
||||
</button>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -8,12 +8,14 @@ import {
|
|||
getUserBetContracts,
|
||||
getUserBetContractsQuery,
|
||||
listAllContracts,
|
||||
trendingContractsQuery,
|
||||
} from 'web/lib/firebase/contracts'
|
||||
import { QueryClient, useQuery, useQueryClient } from 'react-query'
|
||||
import { MINUTE_MS, sleep } from 'common/util/time'
|
||||
import { query, limit } from 'firebase/firestore'
|
||||
import { dailyScoreIndex } from 'web/lib/service/algolia'
|
||||
import {
|
||||
dailyScoreIndex,
|
||||
newIndex,
|
||||
trendingIndex,
|
||||
} from 'web/lib/service/algolia'
|
||||
import { CPMMBinaryContract } from 'common/contract'
|
||||
import { zipObject } from 'lodash'
|
||||
|
||||
|
@ -27,16 +29,50 @@ export const useContracts = () => {
|
|||
return contracts
|
||||
}
|
||||
|
||||
export const useTrendingContracts = (maxContracts: number) => {
|
||||
const { data } = useQuery(['trending-contracts', maxContracts], () =>
|
||||
trendingIndex.search<CPMMBinaryContract>('', {
|
||||
facetFilters: ['isResolved:false'],
|
||||
hitsPerPage: maxContracts,
|
||||
})
|
||||
)
|
||||
if (!data) return undefined
|
||||
return data.hits
|
||||
}
|
||||
|
||||
export const useNewContracts = (maxContracts: number) => {
|
||||
const { data } = useQuery(['newest-contracts', maxContracts], () =>
|
||||
newIndex.search<CPMMBinaryContract>('', {
|
||||
facetFilters: ['isResolved:false'],
|
||||
hitsPerPage: maxContracts,
|
||||
})
|
||||
)
|
||||
if (!data) return undefined
|
||||
return data.hits
|
||||
}
|
||||
|
||||
export const useContractsByDailyScoreNotBetOn = (
|
||||
userId: string | null | undefined,
|
||||
maxContracts: number
|
||||
) => {
|
||||
const { data } = useQuery(['daily-score', userId, maxContracts], () =>
|
||||
dailyScoreIndex.search<CPMMBinaryContract>('', {
|
||||
facetFilters: ['isResolved:false', `uniqueBettors:-${userId}`],
|
||||
hitsPerPage: maxContracts,
|
||||
})
|
||||
)
|
||||
if (!userId || !data) return undefined
|
||||
return data.hits.filter((c) => c.dailyScore)
|
||||
}
|
||||
|
||||
export const useContractsByDailyScoreGroups = (
|
||||
groupSlugs: string[] | undefined
|
||||
) => {
|
||||
const facetFilters = ['isResolved:false']
|
||||
|
||||
const { data } = useQuery(['daily-score', groupSlugs], () =>
|
||||
Promise.all(
|
||||
(groupSlugs ?? []).map((slug) =>
|
||||
dailyScoreIndex.search<CPMMBinaryContract>('', {
|
||||
facetFilters: [...facetFilters, `groupLinks.slug:${slug}`],
|
||||
facetFilters: ['isResolved:false', `groupLinks.slug:${slug}`],
|
||||
})
|
||||
)
|
||||
)
|
||||
|
@ -56,14 +92,6 @@ export const getCachedContracts = async () =>
|
|||
staleTime: Infinity,
|
||||
})
|
||||
|
||||
export const useTrendingContracts = (maxContracts: number) => {
|
||||
const result = useFirestoreQueryData(
|
||||
['trending-contracts', maxContracts],
|
||||
query(trendingContractsQuery, limit(maxContracts))
|
||||
)
|
||||
return result.data
|
||||
}
|
||||
|
||||
export const useInactiveContracts = () => {
|
||||
const [contracts, setContracts] = useState<Contract[] | undefined>()
|
||||
|
||||
|
|
|
@ -1,17 +0,0 @@
|
|||
import { RefObject, useState, useEffect } from 'react'
|
||||
|
||||
// todo: consider consolidation with use-measure-size
|
||||
export const useElementWidth = <T extends Element>(ref: RefObject<T>) => {
|
||||
const [width, setWidth] = useState<number>()
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
setWidth(ref.current?.clientWidth)
|
||||
}
|
||||
handleResize()
|
||||
window.addEventListener('resize', handleResize)
|
||||
return () => {
|
||||
window.removeEventListener('resize', handleResize)
|
||||
}
|
||||
}, [ref])
|
||||
return width
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
import { track } from '@amplitude/analytics-browser'
|
||||
import { useEffect } from 'react'
|
||||
import { track } from 'web/lib/service/analytics'
|
||||
import { inIframe } from './use-is-iframe'
|
||||
|
||||
export const useTracking = (
|
||||
|
@ -10,5 +10,5 @@ export const useTracking = (
|
|||
useEffect(() => {
|
||||
if (excludeIframe && inIframe()) return
|
||||
track(eventName, eventProperties)
|
||||
}, [])
|
||||
}, [eventName, eventProperties, excludeIframe])
|
||||
}
|
||||
|
|
|
@ -14,6 +14,8 @@ export const getIndexName = (sort: string) => {
|
|||
return `${indexPrefix}contracts-${sort}`
|
||||
}
|
||||
|
||||
export const trendingIndex = searchClient.initIndex(getIndexName('score'))
|
||||
export const newIndex = searchClient.initIndex(getIndexName('newest'))
|
||||
export const probChangeDescendingIndex = searchClient.initIndex(
|
||||
getIndexName('prob-change-day')
|
||||
)
|
||||
|
|
|
@ -4,6 +4,7 @@ import { listAllComments } from 'web/lib/firebase/comments'
|
|||
import { getContractFromId } from 'web/lib/firebase/contracts'
|
||||
import { applyCorsHeaders, CORS_UNRESTRICTED } from 'web/lib/api/cors'
|
||||
import { FullMarket, ApiError, toFullMarket } from '../../_types'
|
||||
import { marketCacheStrategy } from '../../markets'
|
||||
|
||||
export default async function handler(
|
||||
req: NextApiRequest,
|
||||
|
@ -24,6 +25,6 @@ export default async function handler(
|
|||
return
|
||||
}
|
||||
|
||||
res.setHeader('Cache-Control', 'max-age=0')
|
||||
res.setHeader('Cache-Control', marketCacheStrategy)
|
||||
return res.status(200).json(toFullMarket(contract, comments, bets))
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ import { NextApiRequest, NextApiResponse } from 'next'
|
|||
import { getContractFromId } from 'web/lib/firebase/contracts'
|
||||
import { applyCorsHeaders, CORS_UNRESTRICTED } from 'web/lib/api/cors'
|
||||
import { ApiError, toLiteMarket, LiteMarket } from '../../_types'
|
||||
import { marketCacheStrategy } from '../../markets'
|
||||
|
||||
export default async function handler(
|
||||
req: NextApiRequest,
|
||||
|
@ -18,6 +19,6 @@ export default async function handler(
|
|||
return
|
||||
}
|
||||
|
||||
res.setHeader('Cache-Control', 'max-age=0')
|
||||
res.setHeader('Cache-Control', marketCacheStrategy)
|
||||
return res.status(200).json(toLiteMarket(contract))
|
||||
}
|
||||
|
|
|
@ -6,6 +6,8 @@ import { toLiteMarket, ValidationError } from './_types'
|
|||
import { z } from 'zod'
|
||||
import { validate } from './_validate'
|
||||
|
||||
export const marketCacheStrategy = 's-maxage=15, stale-while-revalidate=45'
|
||||
|
||||
const queryParams = z
|
||||
.object({
|
||||
limit: z
|
||||
|
@ -39,7 +41,7 @@ export default async function handler(
|
|||
try {
|
||||
const contracts = await listAllContracts(limit, before)
|
||||
// Serve from Vercel cache, then update. see https://vercel.com/docs/concepts/functions/edge-caching
|
||||
res.setHeader('Cache-Control', 's-maxage=1, stale-while-revalidate')
|
||||
res.setHeader('Cache-Control', marketCacheStrategy)
|
||||
res.status(200).json(contracts.map(toLiteMarket))
|
||||
} catch (e) {
|
||||
res.status(400).json({
|
||||
|
|
|
@ -280,25 +280,27 @@ export function NewContract(props: {
|
|||
<label className="label">
|
||||
<span className="mb-1">Answer type</span>
|
||||
</label>
|
||||
<ChoicesToggleGroup
|
||||
currentChoice={outcomeType}
|
||||
setChoice={(choice) => {
|
||||
if (choice === 'FREE_RESPONSE')
|
||||
setMarketInfoText(
|
||||
'Users can submit their own answers to this market.'
|
||||
)
|
||||
else setMarketInfoText('')
|
||||
setOutcomeType(choice as outcomeType)
|
||||
}}
|
||||
choicesMap={{
|
||||
'Yes / No': 'BINARY',
|
||||
// 'Multiple choice': 'MULTIPLE_CHOICE',
|
||||
'Free response': 'FREE_RESPONSE',
|
||||
// Numeric: 'PSEUDO_NUMERIC',
|
||||
}}
|
||||
isSubmitting={isSubmitting}
|
||||
className={'col-span-4'}
|
||||
/>
|
||||
<Row>
|
||||
<ChoicesToggleGroup
|
||||
currentChoice={outcomeType}
|
||||
setChoice={(choice) => {
|
||||
if (choice === 'FREE_RESPONSE')
|
||||
setMarketInfoText(
|
||||
'Users can submit their own answers to this market.'
|
||||
)
|
||||
else setMarketInfoText('')
|
||||
setOutcomeType(choice as outcomeType)
|
||||
}}
|
||||
choicesMap={{
|
||||
'Yes / No': 'BINARY',
|
||||
// 'Multiple choice': 'MULTIPLE_CHOICE',
|
||||
'Free response': 'FREE_RESPONSE',
|
||||
// Numeric: 'PSEUDO_NUMERIC',
|
||||
}}
|
||||
isSubmitting={isSubmitting}
|
||||
className={'col-span-4'}
|
||||
/>
|
||||
</Row>
|
||||
{marketInfoText && (
|
||||
<div className="mt-3 ml-1 text-sm text-indigo-700">
|
||||
{marketInfoText}
|
||||
|
@ -390,23 +392,7 @@ export function NewContract(props: {
|
|||
</>
|
||||
)}
|
||||
|
||||
<div className="form-control mb-1 items-start gap-1">
|
||||
<label className="label gap-2">
|
||||
<span className="mb-1">Visibility</span>
|
||||
<InfoTooltip text="Whether the market will be listed on the home page." />
|
||||
</label>
|
||||
<ChoicesToggleGroup
|
||||
currentChoice={visibility}
|
||||
setChoice={(choice) => setVisibility(choice as visibility)}
|
||||
choicesMap={{
|
||||
Public: 'public',
|
||||
Unlisted: 'unlisted',
|
||||
}}
|
||||
isSubmitting={isSubmitting}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Spacer h={6} />
|
||||
<Spacer h={4} />
|
||||
|
||||
<Row className={'items-end gap-x-2'}>
|
||||
<GroupSelector
|
||||
|
@ -421,6 +407,20 @@ export function NewContract(props: {
|
|||
</SiteLink>
|
||||
)}
|
||||
</Row>
|
||||
|
||||
<Row className="form-control my-2 items-center gap-2 text-sm">
|
||||
<span>Display this market on homepage</span>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={visibility === 'public'}
|
||||
disabled={isSubmitting}
|
||||
className="cursor-pointer"
|
||||
onChange={(e) =>
|
||||
setVisibility(e.target.checked ? 'public' : 'unlisted')
|
||||
}
|
||||
/>
|
||||
</Row>
|
||||
|
||||
<Spacer h={6} />
|
||||
|
||||
<div className="form-control mb-1 items-start">
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { Bet } from 'common/bet'
|
||||
import { Contract } from 'common/contract'
|
||||
import { DOMAIN } from 'common/envs/constants'
|
||||
import { useState } from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { BetInline } from 'web/components/bet-inline'
|
||||
import { Button } from 'web/components/button'
|
||||
import {
|
||||
|
@ -20,7 +20,6 @@ import { SiteLink } from 'web/components/site-link'
|
|||
import { useContractWithPreload } from 'web/hooks/use-contract'
|
||||
import { useMeasureSize } from 'web/hooks/use-measure-size'
|
||||
import { fromPropz, usePropz } from 'web/hooks/use-propz'
|
||||
import { useTracking } from 'web/hooks/use-tracking'
|
||||
import { listAllBets } from 'web/lib/firebase/bets'
|
||||
import {
|
||||
contractPath,
|
||||
|
@ -28,6 +27,7 @@ import {
|
|||
tradingAllowed,
|
||||
} from 'web/lib/firebase/contracts'
|
||||
import Custom404 from '../../404'
|
||||
import { track } from 'web/lib/service/analytics'
|
||||
|
||||
export const getStaticProps = fromPropz(getStaticPropz)
|
||||
export async function getStaticPropz(props: {
|
||||
|
@ -72,11 +72,14 @@ interface EmbedProps {
|
|||
|
||||
export function ContractEmbed(props: EmbedProps) {
|
||||
const { contract } = props
|
||||
useTracking('view market embed', {
|
||||
slug: contract.slug,
|
||||
contractId: contract.id,
|
||||
creatorId: contract.creatorId,
|
||||
})
|
||||
useEffect(() => {
|
||||
track('view market embed', {
|
||||
slug: contract.slug,
|
||||
contractId: contract.id,
|
||||
creatorId: contract.creatorId,
|
||||
hostname: window.location.hostname,
|
||||
})
|
||||
}, [contract.creatorId, contract.id, contract.slug])
|
||||
|
||||
// return (height < 250px) ? Card : SmolView
|
||||
return (
|
||||
|
@ -104,7 +107,7 @@ function ContractSmolView({ contract, bets }: EmbedProps) {
|
|||
|
||||
const href = `https://${DOMAIN}${contractPath(contract)}`
|
||||
|
||||
const { setElem, height: graphHeight } = useMeasureSize()
|
||||
const { setElem, width: graphWidth, height: graphHeight } = useMeasureSize()
|
||||
|
||||
const [betPanelOpen, setBetPanelOpen] = useState(false)
|
||||
|
||||
|
@ -157,7 +160,14 @@ function ContractSmolView({ contract, bets }: EmbedProps) {
|
|||
)}
|
||||
|
||||
<div className="mx-1 mb-2 min-h-0 flex-1" ref={setElem}>
|
||||
<ContractChart contract={contract} bets={bets} height={graphHeight} />
|
||||
{graphWidth != null && graphHeight != null && (
|
||||
<ContractChart
|
||||
contract={contract}
|
||||
bets={bets}
|
||||
width={graphWidth}
|
||||
height={graphHeight}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Col>
|
||||
)
|
||||
|
|
|
@ -12,7 +12,6 @@ import { Dictionary, sortBy, sum } from 'lodash'
|
|||
|
||||
import { Page } from 'web/components/page'
|
||||
import { Col } from 'web/components/layout/col'
|
||||
import { ContractSearch, SORTS } from 'web/components/contract-search'
|
||||
import { User } from 'common/user'
|
||||
import { useTracking } from 'web/hooks/use-tracking'
|
||||
import { track } from 'web/lib/service/analytics'
|
||||
|
@ -43,7 +42,12 @@ import { isArray, keyBy } from 'lodash'
|
|||
import { usePrefetch } from 'web/hooks/use-prefetch'
|
||||
import { Title } from 'web/components/title'
|
||||
import { CPMMBinaryContract } from 'common/contract'
|
||||
import { useContractsByDailyScoreGroups } from 'web/hooks/use-contracts'
|
||||
import {
|
||||
useContractsByDailyScoreNotBetOn,
|
||||
useContractsByDailyScoreGroups,
|
||||
useTrendingContracts,
|
||||
useNewContracts,
|
||||
} from 'web/hooks/use-contracts'
|
||||
import { ProfitBadge } from 'web/components/profit-badge'
|
||||
import { LoadingIndicator } from 'web/components/loading-indicator'
|
||||
|
||||
|
@ -71,12 +75,18 @@ export default function Home() {
|
|||
}
|
||||
}, [user, sections])
|
||||
|
||||
const groups = useMemberGroupsSubscription(user)
|
||||
const trendingContracts = useTrendingContracts(6)
|
||||
const newContracts = useNewContracts(6)
|
||||
const dailyTrendingContracts = useContractsByDailyScoreNotBetOn(user?.id, 6)
|
||||
|
||||
const groups = useMemberGroupsSubscription(user)
|
||||
const groupContracts = useContractsByDailyScoreGroups(
|
||||
groups?.map((g) => g.slug)
|
||||
)
|
||||
|
||||
const isLoading =
|
||||
!user || !trendingContracts || !newContracts || !dailyTrendingContracts
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<Toaster />
|
||||
|
@ -90,11 +100,15 @@ export default function Home() {
|
|||
<DailyStats user={user} />
|
||||
</Row>
|
||||
|
||||
{!user ? (
|
||||
{isLoading ? (
|
||||
<LoadingIndicator />
|
||||
) : (
|
||||
<>
|
||||
{sections.map((section) => renderSection(section, user))}
|
||||
{renderSections(user, sections, {
|
||||
score: trendingContracts,
|
||||
newest: newContracts,
|
||||
'daily-trending': dailyTrendingContracts,
|
||||
})}
|
||||
|
||||
<TrendingGroupsSection user={user} />
|
||||
|
||||
|
@ -118,8 +132,8 @@ export default function Home() {
|
|||
}
|
||||
|
||||
const HOME_SECTIONS = [
|
||||
{ label: 'Daily movers', id: 'daily-movers' },
|
||||
{ label: 'Daily trending', id: 'daily-trending' },
|
||||
{ label: 'Daily movers', id: 'daily-movers' },
|
||||
{ label: 'Trending', id: 'score' },
|
||||
{ label: 'New', id: 'newest' },
|
||||
]
|
||||
|
@ -128,11 +142,7 @@ export const getHomeItems = (sections: string[]) => {
|
|||
// Accommodate old home sections.
|
||||
if (!isArray(sections)) sections = []
|
||||
|
||||
const items: { id: string; label: string; group?: Group }[] = [
|
||||
...HOME_SECTIONS,
|
||||
]
|
||||
const itemsById = keyBy(items, 'id')
|
||||
|
||||
const itemsById = keyBy(HOME_SECTIONS, 'id')
|
||||
const sectionItems = filterDefined(sections.map((id) => itemsById[id]))
|
||||
|
||||
// Add new home section items to the top.
|
||||
|
@ -140,7 +150,9 @@ export const getHomeItems = (sections: string[]) => {
|
|||
...HOME_SECTIONS.filter((item) => !sectionItems.includes(item))
|
||||
)
|
||||
// Add unmentioned items to the end.
|
||||
sectionItems.push(...items.filter((item) => !sectionItems.includes(item)))
|
||||
sectionItems.push(
|
||||
...HOME_SECTIONS.filter((item) => !sectionItems.includes(item))
|
||||
)
|
||||
|
||||
return {
|
||||
sections: sectionItems,
|
||||
|
@ -148,28 +160,46 @@ export const getHomeItems = (sections: string[]) => {
|
|||
}
|
||||
}
|
||||
|
||||
function renderSection(section: { id: string; label: string }, user: User) {
|
||||
const { id, label } = section
|
||||
if (id === 'daily-movers') {
|
||||
return <DailyMoversSection key={id} userId={user.id} />
|
||||
function renderSections(
|
||||
user: User,
|
||||
sections: { id: string; label: string }[],
|
||||
sectionContracts: {
|
||||
'daily-trending': CPMMBinaryContract[]
|
||||
newest: CPMMBinaryContract[]
|
||||
score: CPMMBinaryContract[]
|
||||
}
|
||||
if (id === 'daily-trending')
|
||||
return (
|
||||
<SearchSection
|
||||
key={id}
|
||||
label={label}
|
||||
sort={'daily-score'}
|
||||
pill="personal"
|
||||
user={user}
|
||||
/>
|
||||
)
|
||||
const sort = SORTS.find((sort) => sort.value === id)
|
||||
if (sort)
|
||||
return (
|
||||
<SearchSection key={id} label={label} sort={sort.value} user={user} />
|
||||
)
|
||||
|
||||
return null
|
||||
) {
|
||||
return (
|
||||
<>
|
||||
{sections.map((s) => {
|
||||
const { id, label } = s
|
||||
if (id === 'daily-movers') {
|
||||
return <DailyMoversSection key={id} userId={user.id} />
|
||||
}
|
||||
if (id === 'daily-trending') {
|
||||
return (
|
||||
<ContractsSection
|
||||
key={id}
|
||||
label={label}
|
||||
contracts={sectionContracts[id]}
|
||||
sort="daily-score"
|
||||
showProbChange
|
||||
/>
|
||||
)
|
||||
}
|
||||
const contracts =
|
||||
sectionContracts[s.id as keyof typeof sectionContracts]
|
||||
return (
|
||||
<ContractsSection
|
||||
key={id}
|
||||
label={label}
|
||||
contracts={contracts}
|
||||
sort={id === 'daily-trending' ? 'daily-score' : (id as Sort)}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function renderGroupSections(
|
||||
|
@ -237,13 +267,14 @@ function SectionHeader(props: {
|
|||
)
|
||||
}
|
||||
|
||||
function SearchSection(props: {
|
||||
function ContractsSection(props: {
|
||||
label: string
|
||||
user: User
|
||||
contracts: CPMMBinaryContract[]
|
||||
sort: Sort
|
||||
pill?: string
|
||||
showProbChange?: boolean
|
||||
}) {
|
||||
const { label, user, sort, pill } = props
|
||||
const { label, contracts, sort, pill, showProbChange } = props
|
||||
|
||||
return (
|
||||
<Col>
|
||||
|
@ -251,14 +282,7 @@ function SearchSection(props: {
|
|||
label={label}
|
||||
href={`/search?s=${sort}${pill ? `&p=${pill}` : ''}`}
|
||||
/>
|
||||
<ContractSearch
|
||||
user={user}
|
||||
defaultSort={sort}
|
||||
defaultPill={pill}
|
||||
noControls
|
||||
maxResults={6}
|
||||
persistPrefix={`home-${sort}`}
|
||||
/>
|
||||
<ContractsGrid contracts={contracts} cardUIOptions={{ showProbChange }} />
|
||||
</Col>
|
||||
)
|
||||
}
|
||||
|
|
43
web/pages/labs/index.tsx
Normal file
43
web/pages/labs/index.tsx
Normal file
|
@ -0,0 +1,43 @@
|
|||
import Masonry from 'react-masonry-css'
|
||||
import { Page } from 'web/components/page'
|
||||
import { SiteLink } from 'web/components/site-link'
|
||||
import { Title } from 'web/components/title'
|
||||
|
||||
export default function LabsPage() {
|
||||
return (
|
||||
<Page>
|
||||
<Title text="Manifold Labs" />
|
||||
|
||||
<Masonry
|
||||
breakpointCols={{ default: 2, 768: 1 }}
|
||||
className="-ml-4 flex w-auto"
|
||||
columnClassName="pl-4 bg-clip-padding"
|
||||
>
|
||||
<LabCard
|
||||
title="Dating docs"
|
||||
description="Browse dating docs or create your own"
|
||||
href="/date-docs"
|
||||
/>
|
||||
</Masonry>
|
||||
</Page>
|
||||
)
|
||||
}
|
||||
|
||||
const LabCard = (props: {
|
||||
title: string
|
||||
description: string
|
||||
href: string
|
||||
}) => {
|
||||
const { title, description, href } = props
|
||||
return (
|
||||
<SiteLink
|
||||
href={href}
|
||||
className="group flex h-full w-full flex-col rounded-lg bg-white p-4 shadow-md transition-shadow duration-200 hover:no-underline hover:shadow-lg"
|
||||
>
|
||||
<h3 className="text-lg font-semibold group-hover:underline group-hover:decoration-indigo-400 group-hover:decoration-2">
|
||||
{title}
|
||||
</h3>
|
||||
<p className="mt-2 text-gray-600">{description}</p>
|
||||
</SiteLink>
|
||||
)
|
||||
}
|
|
@ -154,7 +154,6 @@ export function PostComment(props: {
|
|||
smallImage
|
||||
/>
|
||||
<Row className="mt-2 items-center gap-6 text-xs text-gray-500">
|
||||
<Tipper comment={comment} tips={tips ?? {}} />
|
||||
{onReplyClick && (
|
||||
<button
|
||||
className="font-bold hover:underline"
|
||||
|
@ -163,6 +162,7 @@ export function PostComment(props: {
|
|||
Reply
|
||||
</button>
|
||||
)}
|
||||
<Tipper comment={comment} tips={tips ?? {}} />
|
||||
</Row>
|
||||
</div>
|
||||
</Row>
|
||||
|
|
Loading…
Reference in New Issue
Block a user