From 38b7c898f6571b1e3a3e93fe1f921ad743112bfb Mon Sep 17 00:00:00 2001 From: Marshall Polaris Date: Fri, 30 Sep 2022 16:16:04 -0700 Subject: [PATCH 01/25] More refactoring to make chart tooltips more flexible (#975) --- web/components/charts/contract/binary.tsx | 14 ++--- web/components/charts/contract/choice.tsx | 16 +++--- web/components/charts/contract/numeric.tsx | 9 ++-- .../charts/contract/pseudo-numeric.tsx | 16 +++--- web/components/charts/generic-charts.tsx | 53 ++++++++----------- web/components/charts/helpers.tsx | 52 +++++++++++------- 6 files changed, 88 insertions(+), 72 deletions(-) diff --git a/web/components/charts/contract/binary.tsx b/web/components/charts/contract/binary.tsx index a3f04a29..564ef68c 100644 --- a/web/components/charts/contract/binary.tsx +++ b/web/components/charts/contract/binary.tsx @@ -25,19 +25,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>) => { - const { p, xScale } = props - const { x, y, datum } = p +const BinaryChartTooltip = (props: TooltipProps>) => { + const { data, mouseX, xScale } = props const [start, end] = xScale.domain() + const d = xScale.invert(mouseX) return ( - {datum && } - {formatDateInRange(x, start, end)} - {formatPct(y)} + {data.obj && } + {formatDateInRange(d, start, end)} + {formatPct(data.y)} ) } diff --git a/web/components/charts/contract/choice.tsx b/web/components/charts/contract/choice.tsx index 7c9ec07a..665a01cd 100644 --- a/web/components/charts/contract/choice.tsx +++ b/web/components/charts/contract/choice.tsx @@ -114,7 +114,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 @@ -181,12 +181,12 @@ export const ChoiceContractChart = (props: { const yScale = scaleLinear([0, 1], [height - MARGIN_Y, 0]) const ChoiceTooltip = useMemo( - () => (props: TooltipProps>) => { - const { p, xScale } = props - const { x, y, datum } = p + () => (props: TooltipProps>) => { + 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 +197,11 @@ export const ChoiceContractChart = (props: { return ( <> - {datum && } + {data.obj && ( + + )} - {formatDateInRange(x, start, end)} + {formatDateInRange(d, start, end)} diff --git a/web/components/charts/contract/numeric.tsx b/web/components/charts/contract/numeric.tsx index ac300361..f8051308 100644 --- a/web/components/charts/contract/numeric.tsx +++ b/web/components/charts/contract/numeric.tsx @@ -21,12 +21,15 @@ const getNumericChartData = (contract: NumericContract) => { })) } -const NumericChartTooltip = (props: TooltipProps) => { - const { x, y } = props.p +const NumericChartTooltip = ( + props: TooltipProps +) => { + const { data, mouseX, xScale } = props + const x = xScale.invert(mouseX) return ( <> {formatLargeNumber(x)} - {formatPct(y, 2)} + {formatPct(data.y, 2)} ) } diff --git a/web/components/charts/contract/pseudo-numeric.tsx b/web/components/charts/contract/pseudo-numeric.tsx index adf2e493..04b1fafb 100644 --- a/web/components/charts/contract/pseudo-numeric.tsx +++ b/web/components/charts/contract/pseudo-numeric.tsx @@ -37,19 +37,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>) => { - const { p, xScale } = props - const { x, y, datum } = p +const PseudoNumericChartTooltip = ( + props: TooltipProps> +) => { + const { data, mouseX, xScale } = props const [start, end] = xScale.domain() + const d = xScale.invert(mouseX) return ( - {datum && } - {formatDateInRange(x, start, end)} - {formatLargeNumber(y)} + {data.obj && } + {formatDateInRange(d, start, end)} + {formatLargeNumber(data.y)} ) } diff --git a/web/components/charts/generic-charts.tsx b/web/components/charts/generic-charts.tsx index 5ae30ad4..152b264c 100644 --- a/web/components/charts/generic-charts.tsx +++ b/web/components/charts/generic-charts.tsx @@ -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 = >( + data: P[], + xScale: ContinuousScale +) => { + 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 =

(props: { data: P[] w: number @@ -39,7 +53,7 @@ export const DistributionChart =

(props: { xScale: ScaleContinuousNumeric yScale: ScaleContinuousNumeric onMouseOver?: (p: P | undefined) => void - Tooltip?: TooltipComponent

+ Tooltip?: TooltipComponent }) => { const { color, data, yScale, w, h, Tooltip } = props @@ -50,7 +64,6 @@ export const DistributionChart =

(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(xScale).ticks(w / 100) @@ -58,6 +71,8 @@ export const DistributionChart =

(props: { return { xAxis, yAxis } }, [w, xScale, yScale]) + const onMouseOver = useEvent(betAtPointSelector(data, xScale)) + const onSelect = useEvent((ev: D3BrushEvent

) => { if (ev.selection) { const [mouseX0, mouseX1] = ev.selection as [number, number] @@ -69,14 +84,6 @@ export const DistributionChart =

(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 ( (props: { xScale: ScaleTime yScale: ScaleContinuousNumeric onMouseOver?: (p: P | undefined) => void - Tooltip?: TooltipComponent

+ Tooltip?: TooltipComponent pct?: boolean }) => { const { colors, data, yScale, w, h, Tooltip, pct } = props @@ -119,7 +126,6 @@ export const MultiValueHistoryChart =

(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 =

(props: { return d3Stack(data) }, [data]) + const onMouseOver = useEvent(betAtPointSelector(data, xScale)) + const onSelect = useEvent((ev: D3BrushEvent

) => { if (ev.selection) { const [mouseX0, mouseX1] = ev.selection as [number, number] @@ -152,14 +160,6 @@ export const MultiValueHistoryChart =

(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 ( (props: { xScale: ScaleTime yScale: ScaleContinuousNumeric onMouseOver?: (p: P | undefined) => void - Tooltip?: TooltipComponent

+ Tooltip?: TooltipComponent pct?: boolean }) => { const { color, data, yScale, w, h, Tooltip, pct } = props @@ -204,7 +204,6 @@ export const SingleValueHistoryChart =

(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 =

(props: { return { xAxis, yAxis } }, [w, h, pct, xScale, yScale]) + const onMouseOver = useEvent(betAtPointSelector(data, xScale)) + const onSelect = useEvent((ev: D3BrushEvent

) => { if (ev.selection) { const [mouseX0, mouseX1] = ev.selection as [number, number] @@ -229,14 +230,6 @@ export const SingleValueHistoryChart =

(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 ( = { x: X; y: Y; datum?: T } +export type Point = { x: X; y: Y; obj?: T } + +export interface ContinuousScale extends AxisScale { + invert(n: number): T +} + export type XScale

= P extends Point ? AxisScale : never export type YScale

= P extends Point ? AxisScale : never @@ -118,18 +123,18 @@ export const AreaWithTopStroke = (props: { ) } -export const SVGChart = >(props: { +export const SVGChart = (props: { children: ReactNode w: number h: number xAxis: Axis yAxis: Axis onSelect?: (ev: D3BrushEvent) => void - onMouseOver?: (mouseX: number, mouseY: number) => P | undefined - Tooltip?: TooltipComponent

+ onMouseOver?: (mouseX: number, mouseY: number) => TT | undefined + Tooltip?: TooltipComponent }) => { 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 overlayRef = useRef(null) const innerW = w - MARGIN_X const innerH = h - MARGIN_Y @@ -148,7 +153,7 @@ export const SVGChart = >(props: { if (!justSelected.current) { justSelected.current = true onSelect(ev) - setMouseState(undefined) + setMouse(undefined) if (overlayRef.current) { select(overlayRef.current).call(brush.clear) } @@ -168,26 +173,32 @@ export const SVGChart = >(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 (

- {mouseState && Tooltip && ( - - + {mouse && Tooltip && ( + + )} @@ -243,8 +254,13 @@ export const getTooltipPosition = ( return result } -export type TooltipProps

= { p: P; xScale: XScale

} -export type TooltipComponent

= React.ComponentType> +export type TooltipProps = { + mouseX: number + mouseY: number + xScale: ContinuousScale + data: T +} +export type TooltipComponent = React.ComponentType> export const TooltipContainer = (props: { pos: TooltipPosition className?: string From 89e26d077e58565afbb4a5d98ecbcd1af53dbed7 Mon Sep 17 00:00:00 2001 From: Marshall Polaris Date: Fri, 30 Sep 2022 16:57:48 -0700 Subject: [PATCH 02/25] Clean up chart sizing code (#977) * Clean up chart sizing code * Do all the chart sizing work in same batch --- web/components/charts/contract/binary.tsx | 40 ++++------ web/components/charts/contract/choice.tsx | 39 ++++----- web/components/charts/contract/index.tsx | 3 +- web/components/charts/contract/numeric.tsx | 37 ++++----- .../charts/contract/pseudo-numeric.tsx | 44 ++++------ web/components/contract/contract-overview.tsx | 80 +++++++++++++++---- web/hooks/use-element-width.tsx | 17 ---- web/pages/embed/[username]/[contractSlug].tsx | 11 ++- 8 files changed, 136 insertions(+), 135 deletions(-) delete mode 100644 web/hooks/use-element-width.tsx diff --git a/web/components/charts/contract/binary.tsx b/web/components/charts/contract/binary.tsx index 564ef68c..7e192767 100644 --- a/web/components/charts/contract/binary.tsx +++ b/web/components/charts/contract/binary.tsx @@ -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' @@ -45,10 +43,11 @@ const BinaryChartTooltip = (props: TooltipProps>) => { export const BinaryContractChart = (props: { contract: BinaryContract bets: Bet[] - height?: number + width: number + height: number onMouseOver?: (p: HistoryPoint | 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(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 ( -

- {width > 0 && ( - - )} -
+ ) } diff --git a/web/components/charts/contract/choice.tsx b/web/components/charts/contract/choice.tsx index 665a01cd..65279b70 100644 --- a/web/components/charts/contract/choice.tsx +++ b/web/components/charts/contract/choice.tsx @@ -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' @@ -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 | 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,10 +172,6 @@ export const ChoiceContractChart = (props: { Date.now() ) const visibleRange = [start, rightmostDate] - const isMobile = useIsMobile(800) - const containerRef = useRef(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]) @@ -212,20 +207,16 @@ export const ChoiceContractChart = (props: { ) return ( -
- {width > 0 && ( - - )} -
+ ) } diff --git a/web/components/charts/contract/index.tsx b/web/components/charts/contract/index.tsx index 1f580bae..1efe1eb4 100644 --- a/web/components/charts/contract/index.tsx +++ b/web/components/charts/contract/index.tsx @@ -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) { diff --git a/web/components/charts/contract/numeric.tsx b/web/components/charts/contract/numeric.tsx index f8051308..2d62cb11 100644 --- a/web/components/charts/contract/numeric.tsx +++ b/web/components/charts/contract/numeric.tsx @@ -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 @@ -36,33 +34,26 @@ const NumericChartTooltip = ( 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(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 ( -
- {width > 0 && ( - - )} -
+ ) } diff --git a/web/components/charts/contract/pseudo-numeric.tsx b/web/components/charts/contract/pseudo-numeric.tsx index 04b1fafb..e03d4ad9 100644 --- a/web/components/charts/contract/pseudo-numeric.tsx +++ b/web/components/charts/contract/pseudo-numeric.tsx @@ -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' @@ -59,10 +57,11 @@ const PseudoNumericChartTooltip = ( export const PseudoNumericContractChart = (props: { contract: PseudoNumericContract bets: Bet[] - height?: number + width: number + height: number onMouseOver?: (p: HistoryPoint | 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( @@ -86,30 +85,21 @@ export const PseudoNumericContractChart = (props: { Date.now() ) const visibleRange = [start, rightmostDate] - const isMobile = useIsMobile(800) - const containerRef = useRef(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 ( -
- {width > 0 && ( - - )} -
+ ) } diff --git a/web/components/contract/contract-overview.tsx b/web/components/contract/contract-overview.tsx index add9ba48..3e5f22b2 100644 --- a/web/components/contract/contract-overview.tsx +++ b/web/components/contract/contract-overview.tsx @@ -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(null) + const [chartWidth, setChartWidth] = useState() + const [chartHeight, setChartHeight] = useState() + 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 ( +
+ {chartWidth != null && chartHeight != null && ( + + )} +
+ ) +} + +const NumericOverview = (props: { contract: NumericContract; bets: Bet[] }) => { + const { contract, bets } = props return ( @@ -66,7 +96,12 @@ const NumericOverview = (props: { contract: NumericContract }) => { contract={contract} /> - + ) } @@ -86,7 +121,12 @@ const BinaryOverview = (props: { contract: BinaryContract; bets: Bet[] }) => { /> - + {tradingAllowed(contract) && ( @@ -111,9 +151,12 @@ const ChoiceOverview = (props: { )} - - - + ) } @@ -139,7 +182,12 @@ const PseudoNumericOverview = (props: { {tradingAllowed(contract) && } - + ) } @@ -153,7 +201,7 @@ export const ContractOverview = (props: { case 'BINARY': return case 'NUMERIC': - return + return case 'PSEUDO_NUMERIC': return case 'FREE_RESPONSE': diff --git a/web/hooks/use-element-width.tsx b/web/hooks/use-element-width.tsx deleted file mode 100644 index 1c373839..00000000 --- a/web/hooks/use-element-width.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { RefObject, useState, useEffect } from 'react' - -// todo: consider consolidation with use-measure-size -export const useElementWidth = (ref: RefObject) => { - const [width, setWidth] = useState() - useEffect(() => { - const handleResize = () => { - setWidth(ref.current?.clientWidth) - } - handleResize() - window.addEventListener('resize', handleResize) - return () => { - window.removeEventListener('resize', handleResize) - } - }, [ref]) - return width -} diff --git a/web/pages/embed/[username]/[contractSlug].tsx b/web/pages/embed/[username]/[contractSlug].tsx index e925a1f6..cc4bc09d 100644 --- a/web/pages/embed/[username]/[contractSlug].tsx +++ b/web/pages/embed/[username]/[contractSlug].tsx @@ -79,7 +79,7 @@ export function ContractEmbed(props: { contract: Contract; bets: Bet[] }) { const href = `https://${DOMAIN}${contractPath(contract)}` - const { setElem, height: graphHeight } = useMeasureSize() + const { setElem, width: graphWidth, height: graphHeight } = useMeasureSize() const [betPanelOpen, setBetPanelOpen] = useState(false) @@ -132,7 +132,14 @@ export function ContractEmbed(props: { contract: Contract; bets: Bet[] }) { )}
- + {graphWidth != null && graphHeight != null && ( + + )}
) From dc0b6dc6a6917c2bf6976e0b8a1879056787f0fd Mon Sep 17 00:00:00 2001 From: Marshall Polaris Date: Fri, 30 Sep 2022 18:01:48 -0700 Subject: [PATCH 03/25] Don't render stuff whenever window size changes (#978) --- web/components/amount-input.tsx | 5 ++--- web/components/bet-panel.tsx | 8 +------- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/web/components/amount-input.tsx b/web/components/amount-input.tsx index 1c9d1c3b..dc5a6124 100644 --- a/web/components/amount-input.tsx +++ b/web/components/amount-input.tsx @@ -5,7 +5,7 @@ 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 { useIsMobile } from 'web/hooks/use-is-mobile' import { Row } from './layout/row' export function AmountInput(props: { @@ -36,8 +36,7 @@ export function AmountInput(props: { onChange(isInvalid ? undefined : amount) } - const { width } = useWindowSize() - const isMobile = (width ?? 0) < 768 + const isMobile = useIsMobile(768) return ( <> diff --git a/web/components/bet-panel.tsx b/web/components/bet-panel.tsx index e93c0e62..beb7168a 100644 --- a/web/components/bet-panel.tsx +++ b/web/components/bet-panel.tsx @@ -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(10) const [error, setError] = useState() const [isSubmitting, setIsSubmitting] = useState(false) From b0b1d72ba61382c2035c8867e735816b0bfaf2f8 Mon Sep 17 00:00:00 2001 From: James Grugett Date: Fri, 30 Sep 2022 20:07:49 -0500 Subject: [PATCH 04/25] Cleaner home page loading! --- web/hooks/use-contracts.ts | 56 ++++++++++++++----- web/lib/service/algolia.ts | 2 + web/pages/home/index.tsx | 112 ++++++++++++++++++++++--------------- 3 files changed, 112 insertions(+), 58 deletions(-) diff --git a/web/hooks/use-contracts.ts b/web/hooks/use-contracts.ts index 11aae65c..7952deba 100644 --- a/web/hooks/use-contracts.ts +++ b/web/hooks/use-contracts.ts @@ -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('', { + 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('', { + 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('', { + 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('', { - 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() diff --git a/web/lib/service/algolia.ts b/web/lib/service/algolia.ts index 29cbd6bf..bdace399 100644 --- a/web/lib/service/algolia.ts +++ b/web/lib/service/algolia.ts @@ -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') ) diff --git a/web/pages/home/index.tsx b/web/pages/home/index.tsx index 2ddc3026..4ad7f97b 100644 --- a/web/pages/home/index.tsx +++ b/web/pages/home/index.tsx @@ -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 ( @@ -90,11 +100,15 @@ export default function Home() { - {!user ? ( + {isLoading ? ( ) : ( <> - {sections.map((section) => renderSection(section, user))} + {renderSections(user, sections, { + score: trendingContracts, + newest: newContracts, + 'daily-trending': dailyTrendingContracts, + })} @@ -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 +function renderSections( + user: User, + sections: { id: string; label: string }[], + sectionContracts: { + 'daily-trending': CPMMBinaryContract[] + newest: CPMMBinaryContract[] + score: CPMMBinaryContract[] } - if (id === 'daily-trending') - return ( - - ) - const sort = SORTS.find((sort) => sort.value === id) - if (sort) - return ( - - ) - - return null +) { + return ( + <> + {sections.map((s) => { + const { id, label } = s + if (id === 'daily-movers') { + return + } + if (id === 'daily-trending') { + return ( + + ) + } + const contracts = + sectionContracts[s.id as keyof typeof sectionContracts] + return ( + + ) + })} + + ) } 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 ( @@ -251,14 +282,7 @@ function SearchSection(props: { label={label} href={`/search?s=${sort}${pill ? `&p=${pill}` : ''}`} /> - + ) } From 2f3ae5192eff205728ffe3063ffcdf30160d50bb Mon Sep 17 00:00:00 2001 From: mantikoros Date: Fri, 30 Sep 2022 20:30:45 -0500 Subject: [PATCH 05/25] embed: disable clicking contract details --- web/components/contract/contract-details.tsx | 42 +++++++++++++++----- 1 file changed, 31 insertions(+), 11 deletions(-) diff --git a/web/components/contract/contract-details.tsx b/web/components/contract/contract-details.tsx index b06c6381..d0aa0ee9 100644 --- a/web/components/contract/contract-details.tsx +++ b/web/components/contract/contract-details.tsx @@ -183,6 +183,7 @@ export function MarketSubheader(props: { contract={contract} resolvedDate={resolvedDate} isCreator={isCreator} + disabled={disabled} /> {!isMobile && ( @@ -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} /> )} @@ -245,7 +248,8 @@ export function MarketGroups(props: { return ( <> - + + {!disabled && user && ( -
+ ) } diff --git a/web/components/contract/tip-button.tsx b/web/components/contract/tip-button.tsx new file mode 100644 index 00000000..79059195 --- /dev/null +++ b/web/components/contract/tip-button.tsx @@ -0,0 +1,57 @@ +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 ( + + + + ) +} diff --git a/web/components/feed/feed-comments.tsx b/web/components/feed/feed-comments.tsx index 20d124f8..b9387a03 100644 --- a/web/components/feed/feed-comments.tsx +++ b/web/components/feed/feed-comments.tsx @@ -177,10 +177,6 @@ export function FeedComment(props: { smallImage /> - {tips && } - {(contract.openCommentBounties ?? 0) > 0 && ( - - )} {onReplyClick && (
diff --git a/web/components/tipper.tsx b/web/components/tipper.tsx index 1dcb0f05..a9c35937 100644 --- a/web/components/tipper.tsx +++ b/web/components/tipper.tsx @@ -1,20 +1,14 @@ -import { - ChevronDoubleRightIcon, - ChevronLeftIcon, - ChevronRightIcon, -} from '@heroicons/react/solid' -import clsx from 'clsx' +import { debounce } from 'lodash' +import { useEffect, useRef, useState } from 'react' + 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 @@ -26,6 +20,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(() => { @@ -35,8 +30,6 @@ export function Tipper(prop: { comment: Comment; tips: CommentTips }) { } }, [tips, myId]) - const total = sum(Object.values(tips)) - savedTip + localTip - // declare debounced function only on first render const [saveTip] = useState(() => debounce(async (user: User, comment: Comment, change: number) => { @@ -80,69 +73,22 @@ export function Tipper(prop: { comment: Comment; tips: CommentTips }) { me && saveTip(me, comment, localTip - savedTip + delta) } - const canDown = me && localTip > savedTip - const canUp = me && me.id !== comment.userId && me.balance >= localTip + 5 + if (me && comment.userId === me.id) { + return <> + } + + const canUp = me && me.balance >= localTip + TIP_SIZE + return ( - addTip(-TIP_SIZE) : undefined} /> - {Math.floor(total)} - addTip(+TIP_SIZE) : undefined} - value={localTip} + addTip(+TIP_SIZE)} + userTipped={localTip > 0} + disabled={!canUp} + isCompact /> - {localTip === 0 ? ( - '' - ) : ( - 0 ? 'text-primary' : 'text-red-400' - )} - > - ({formatMoney(localTip)} tip) - - )} ) } - -function DownTip(props: { onClick?: () => void }) { - const { onClick } = props - return ( - - - - ) -} - -function UpTip(props: { onClick?: () => void; value: number }) { - const { onClick, value } = props - const IconKind = value > TIP_SIZE ? ChevronDoubleRightIcon : ChevronRightIcon - return ( - - - - ) -} diff --git a/web/posts/post-comments.tsx b/web/posts/post-comments.tsx index f1d50a29..74fbb300 100644 --- a/web/posts/post-comments.tsx +++ b/web/posts/post-comments.tsx @@ -154,7 +154,6 @@ export function PostComment(props: { smallImage /> - {onReplyClick && (