Merge branch 'main' of https://github.com/manifoldmarkets/manifold
This commit is contained in:
		
						commit
						603201a00f
					
				|  | @ -1,139 +0,0 @@ | |||
| import { Point, ResponsiveLine } from '@nivo/line' | ||||
| import clsx from 'clsx' | ||||
| import { formatPercent } from 'common/util/format' | ||||
| import dayjs from 'dayjs' | ||||
| import { zip } from 'lodash' | ||||
| import { useWindowSize } from 'web/hooks/use-window-size' | ||||
| import { Col } from '../layout/col' | ||||
| 
 | ||||
| export function DailyCountChart(props: { | ||||
|   startDate: number | ||||
|   dailyCounts: number[] | ||||
|   small?: boolean | ||||
| }) { | ||||
|   const { dailyCounts, startDate, small } = props | ||||
|   const { width } = useWindowSize() | ||||
| 
 | ||||
|   const dates = dailyCounts.map((_, i) => | ||||
|     dayjs(startDate).add(i, 'day').toDate() | ||||
|   ) | ||||
| 
 | ||||
|   const points = zip(dates, dailyCounts).map(([date, betCount]) => ({ | ||||
|     x: date, | ||||
|     y: betCount, | ||||
|   })) | ||||
|   const data = [{ id: 'Count', data: points, color: '#11b981' }] | ||||
| 
 | ||||
|   const bottomAxisTicks = width && width < 600 ? 6 : undefined | ||||
| 
 | ||||
|   return ( | ||||
|     <div | ||||
|       className={clsx( | ||||
|         'h-[250px] w-full overflow-hidden', | ||||
|         !small && 'md:h-[400px]' | ||||
|       )} | ||||
|     > | ||||
|       <ResponsiveLine | ||||
|         data={data} | ||||
|         yScale={{ type: 'linear', stacked: false }} | ||||
|         xScale={{ | ||||
|           type: 'time', | ||||
|         }} | ||||
|         axisBottom={{ | ||||
|           tickValues: bottomAxisTicks, | ||||
|           format: (date) => dayjs(date).format('MMM DD'), | ||||
|         }} | ||||
|         colors={{ datum: 'color' }} | ||||
|         pointSize={0} | ||||
|         pointBorderWidth={1} | ||||
|         pointBorderColor="#fff" | ||||
|         enableSlices="x" | ||||
|         enableGridX={!!width && width >= 800} | ||||
|         enableArea | ||||
|         margin={{ top: 20, right: 28, bottom: 22, left: 40 }} | ||||
|         sliceTooltip={({ slice }) => { | ||||
|           const point = slice.points[0] | ||||
|           return <Tooltip point={point} /> | ||||
|         }} | ||||
|       /> | ||||
|     </div> | ||||
|   ) | ||||
| } | ||||
| 
 | ||||
| export function DailyPercentChart(props: { | ||||
|   startDate: number | ||||
|   dailyPercent: number[] | ||||
|   small?: boolean | ||||
|   excludeFirstDays?: number | ||||
| }) { | ||||
|   const { dailyPercent, startDate, small, excludeFirstDays } = props | ||||
|   const { width } = useWindowSize() | ||||
| 
 | ||||
|   const dates = dailyPercent.map((_, i) => | ||||
|     dayjs(startDate).add(i, 'day').toDate() | ||||
|   ) | ||||
| 
 | ||||
|   const points = zip(dates, dailyPercent) | ||||
|     .map(([date, percent]) => ({ | ||||
|       x: date, | ||||
|       y: percent, | ||||
|     })) | ||||
|     .slice(excludeFirstDays ?? 0) | ||||
|   const data = [{ id: 'Percent', data: points, color: '#11b981' }] | ||||
| 
 | ||||
|   const bottomAxisTicks = width && width < 600 ? 6 : undefined | ||||
| 
 | ||||
|   return ( | ||||
|     <div | ||||
|       className={clsx( | ||||
|         'h-[250px] w-full overflow-hidden', | ||||
|         !small && 'md:h-[400px]' | ||||
|       )} | ||||
|     > | ||||
|       <ResponsiveLine | ||||
|         data={data} | ||||
|         yScale={{ type: 'linear', stacked: false }} | ||||
|         xScale={{ | ||||
|           type: 'time', | ||||
|         }} | ||||
|         axisLeft={{ | ||||
|           format: formatPercent, | ||||
|         }} | ||||
|         axisBottom={{ | ||||
|           tickValues: bottomAxisTicks, | ||||
|           format: (date) => dayjs(date).format('MMM DD'), | ||||
|         }} | ||||
|         colors={{ datum: 'color' }} | ||||
|         pointSize={0} | ||||
|         pointBorderWidth={1} | ||||
|         pointBorderColor="#fff" | ||||
|         enableSlices="x" | ||||
|         enableGridX={!!width && width >= 800} | ||||
|         enableArea | ||||
|         margin={{ top: 20, right: 28, bottom: 22, left: 40 }} | ||||
|         sliceTooltip={({ slice }) => { | ||||
|           const point = slice.points[0] | ||||
|           return <Tooltip point={point} isPercent /> | ||||
|         }} | ||||
|       /> | ||||
|     </div> | ||||
|   ) | ||||
| } | ||||
| 
 | ||||
| function Tooltip(props: { point: Point; isPercent?: boolean }) { | ||||
|   const { point, isPercent } = props | ||||
|   return ( | ||||
|     <Col className="border border-gray-300 bg-white py-2 px-3"> | ||||
|       <div | ||||
|         className="pb-1" | ||||
|         style={{ | ||||
|           color: point.serieColor, | ||||
|         }} | ||||
|       > | ||||
|         <strong>{point.serieId}</strong>{' '} | ||||
|         {isPercent ? formatPercent(+point.data.y) : Math.round(+point.data.y)} | ||||
|       </div> | ||||
|       <div>{dayjs(point.data.x).format('MMM DD')}</div> | ||||
|     </Col> | ||||
|   ) | ||||
| } | ||||
|  | @ -1,6 +1,7 @@ | |||
| import { useMemo } from 'react' | ||||
| import { last, sortBy } from 'lodash' | ||||
| import { scaleTime, scaleLinear } from 'd3-scale' | ||||
| import { curveStepAfter } from 'd3-shape' | ||||
| 
 | ||||
| import { Bet } from 'common/bet' | ||||
| import { getProbability, getInitialProbability } from 'common/calculate' | ||||
|  | @ -76,6 +77,7 @@ export const BinaryContractChart = (props: { | |||
|       yScale={yScale} | ||||
|       data={data} | ||||
|       color="#11b981" | ||||
|       curve={curveStepAfter} | ||||
|       onMouseOver={onMouseOver} | ||||
|       Tooltip={BinaryChartTooltip} | ||||
|       pct | ||||
|  |  | |||
|  | @ -1,6 +1,7 @@ | |||
| import { useMemo } from 'react' | ||||
| import { last, sum, sortBy, groupBy } from 'lodash' | ||||
| import { scaleTime, scaleLinear } from 'd3-scale' | ||||
| import { curveStepAfter } from 'd3-shape' | ||||
| 
 | ||||
| import { Bet } from 'common/bet' | ||||
| import { Answer } from 'common/answer' | ||||
|  | @ -214,6 +215,7 @@ export const ChoiceContractChart = (props: { | |||
|       yScale={yScale} | ||||
|       data={data} | ||||
|       colors={CATEGORY_COLORS} | ||||
|       curve={curveStepAfter} | ||||
|       onMouseOver={onMouseOver} | ||||
|       Tooltip={ChoiceTooltip} | ||||
|       pct | ||||
|  |  | |||
|  | @ -1,6 +1,7 @@ | |||
| import { useMemo } from 'react' | ||||
| import { last, sortBy } from 'lodash' | ||||
| import { scaleTime, scaleLog, scaleLinear } from 'd3-scale' | ||||
| import { curveStepAfter } from 'd3-shape' | ||||
| 
 | ||||
| import { Bet } from 'common/bet' | ||||
| import { DAY_MS } from 'common/util/time' | ||||
|  | @ -85,11 +86,11 @@ export const PseudoNumericContractChart = (props: { | |||
|     Date.now() | ||||
|   ) | ||||
|   const visibleRange = [start, rightmostDate] | ||||
|   const xScale = scaleTime(visibleRange, [0, width ?? 0 - MARGIN_X]) | ||||
|   const xScale = scaleTime(visibleRange, [0, width - MARGIN_X]) | ||||
|   // clamp log scale to make sure zeroes go to the bottom
 | ||||
|   const yScale = isLogScale | ||||
|     ? scaleLog([Math.max(min, 1), max], [height ?? 0 - MARGIN_Y, 0]).clamp(true) | ||||
|     : scaleLinear([min, max], [height ?? 0 - MARGIN_Y, 0]) | ||||
|     ? scaleLog([Math.max(min, 1), max], [height - MARGIN_Y, 0]).clamp(true) | ||||
|     : scaleLinear([min, max], [height - MARGIN_Y, 0]) | ||||
|   return ( | ||||
|     <SingleValueHistoryChart | ||||
|       w={width} | ||||
|  | @ -97,6 +98,7 @@ export const PseudoNumericContractChart = (props: { | |||
|       xScale={xScale} | ||||
|       yScale={yScale} | ||||
|       data={data} | ||||
|       curve={curveStepAfter} | ||||
|       onMouseOver={onMouseOver} | ||||
|       Tooltip={PseudoNumericChartTooltip} | ||||
|       color={NUMERIC_GRAPH_COLOR} | ||||
|  |  | |||
|  | @ -4,11 +4,11 @@ import { axisBottom, axisLeft } from 'd3-axis' | |||
| import { D3BrushEvent } from 'd3-brush' | ||||
| import { ScaleTime, ScaleContinuousNumeric } from 'd3-scale' | ||||
| import { | ||||
|   CurveFactory, | ||||
|   SeriesPoint, | ||||
|   curveLinear, | ||||
|   curveStepAfter, | ||||
|   stack, | ||||
|   stackOrderReverse, | ||||
|   SeriesPoint, | ||||
| } from 'd3-shape' | ||||
| import { range } from 'lodash' | ||||
| 
 | ||||
|  | @ -52,10 +52,11 @@ export const DistributionChart = <P extends DistributionPoint>(props: { | |||
|   color: string | ||||
|   xScale: ScaleContinuousNumeric<number, number> | ||||
|   yScale: ScaleContinuousNumeric<number, number> | ||||
|   curve?: CurveFactory | ||||
|   onMouseOver?: (p: P | undefined) => void | ||||
|   Tooltip?: TooltipComponent<number, P> | ||||
| }) => { | ||||
|   const { color, data, yScale, w, h, Tooltip } = props | ||||
|   const { color, data, yScale, w, h, curve, Tooltip } = props | ||||
| 
 | ||||
|   const [viewXScale, setViewXScale] = | ||||
|     useState<ScaleContinuousNumeric<number, number>>() | ||||
|  | @ -100,7 +101,7 @@ export const DistributionChart = <P extends DistributionPoint>(props: { | |||
|         px={px} | ||||
|         py0={py0} | ||||
|         py1={py1} | ||||
|         curve={curveLinear} | ||||
|         curve={curve ?? curveLinear} | ||||
|       /> | ||||
|     </SVGChart> | ||||
|   ) | ||||
|  | @ -113,11 +114,12 @@ export const MultiValueHistoryChart = <P extends MultiPoint>(props: { | |||
|   colors: readonly string[] | ||||
|   xScale: ScaleTime<number, number> | ||||
|   yScale: ScaleContinuousNumeric<number, number> | ||||
|   curve?: CurveFactory | ||||
|   onMouseOver?: (p: P | undefined) => void | ||||
|   Tooltip?: TooltipComponent<Date, P> | ||||
|   pct?: boolean | ||||
| }) => { | ||||
|   const { colors, data, yScale, w, h, Tooltip, pct } = props | ||||
|   const { colors, data, yScale, w, h, curve, Tooltip, pct } = props | ||||
| 
 | ||||
|   const [viewXScale, setViewXScale] = useState<ScaleTime<number, number>>() | ||||
|   const xScale = viewXScale ?? props.xScale | ||||
|  | @ -177,7 +179,7 @@ export const MultiValueHistoryChart = <P extends MultiPoint>(props: { | |||
|           px={px} | ||||
|           py0={py0} | ||||
|           py1={py1} | ||||
|           curve={curveStepAfter} | ||||
|           curve={curve ?? curveLinear} | ||||
|           fill={colors[i]} | ||||
|         /> | ||||
|       ))} | ||||
|  | @ -192,11 +194,12 @@ export const SingleValueHistoryChart = <P extends HistoryPoint>(props: { | |||
|   color: string | ||||
|   xScale: ScaleTime<number, number> | ||||
|   yScale: ScaleContinuousNumeric<number, number> | ||||
|   curve?: CurveFactory | ||||
|   onMouseOver?: (p: P | undefined) => void | ||||
|   Tooltip?: TooltipComponent<Date, P> | ||||
|   pct?: boolean | ||||
| }) => { | ||||
|   const { color, data, yScale, w, h, Tooltip, pct } = props | ||||
|   const { color, data, yScale, w, h, curve, Tooltip, pct } = props | ||||
| 
 | ||||
|   const [viewXScale, setViewXScale] = useState<ScaleTime<number, number>>() | ||||
|   const xScale = viewXScale ?? props.xScale | ||||
|  | @ -246,7 +249,7 @@ export const SingleValueHistoryChart = <P extends HistoryPoint>(props: { | |||
|         px={px} | ||||
|         py0={py0} | ||||
|         py1={py1} | ||||
|         curve={curveStepAfter} | ||||
|         curve={curve ?? curveLinear} | ||||
|       /> | ||||
|     </SVGChart> | ||||
|   ) | ||||
|  |  | |||
|  | @ -10,7 +10,7 @@ import { | |||
| import { pointer, select } from 'd3-selection' | ||||
| import { Axis, AxisScale } from 'd3-axis' | ||||
| import { brushX, D3BrushEvent } from 'd3-brush' | ||||
| import { area, line, curveStepAfter, CurveFactory } from 'd3-shape' | ||||
| import { area, line, CurveFactory } from 'd3-shape' | ||||
| import { nanoid } from 'nanoid' | ||||
| import dayjs from 'dayjs' | ||||
| import clsx from 'clsx' | ||||
|  | @ -73,11 +73,11 @@ const LinePathInternal = <P,>( | |||
|     data: P[] | ||||
|     px: number | ((p: P) => number) | ||||
|     py: number | ((p: P) => number) | ||||
|     curve?: CurveFactory | ||||
|     curve: CurveFactory | ||||
|   } & SVGProps<SVGPathElement> | ||||
| ) => { | ||||
|   const { data, px, py, curve, ...rest } = props | ||||
|   const d3Line = line<P>(px, py).curve(curve ?? curveStepAfter) | ||||
|   const d3Line = line<P>(px, py).curve(curve) | ||||
|   // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
 | ||||
|   return <path {...rest} fill="none" d={d3Line(data)!} /> | ||||
| } | ||||
|  | @ -89,11 +89,11 @@ const AreaPathInternal = <P,>( | |||
|     px: number | ((p: P) => number) | ||||
|     py0: number | ((p: P) => number) | ||||
|     py1: number | ((p: P) => number) | ||||
|     curve?: CurveFactory | ||||
|     curve: CurveFactory | ||||
|   } & SVGProps<SVGPathElement> | ||||
| ) => { | ||||
|   const { data, px, py0, py1, curve, ...rest } = props | ||||
|   const d3Area = area<P>(px, py0, py1).curve(curve ?? curveStepAfter) | ||||
|   const d3Area = area<P>(px, py0, py1).curve(curve) | ||||
|   // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
 | ||||
|   return <path {...rest} d={d3Area(data)!} /> | ||||
| } | ||||
|  | @ -105,7 +105,7 @@ export const AreaWithTopStroke = <P,>(props: { | |||
|   px: number | ((p: P) => number) | ||||
|   py0: number | ((p: P) => number) | ||||
|   py1: number | ((p: P) => number) | ||||
|   curve?: CurveFactory | ||||
|   curve: CurveFactory | ||||
| }) => { | ||||
|   const { color, data, px, py0, py1, curve } = props | ||||
|   return ( | ||||
|  |  | |||
							
								
								
									
										76
									
								
								web/components/charts/stats.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								web/components/charts/stats.tsx
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,76 @@ | |||
| import { useMemo } from 'react' | ||||
| import { scaleTime, scaleLinear } from 'd3-scale' | ||||
| import { min, max } from 'lodash' | ||||
| import dayjs from 'dayjs' | ||||
| 
 | ||||
| import { formatPercent } from 'common/util/format' | ||||
| import { Row } from '../layout/row' | ||||
| import { HistoryPoint, SingleValueHistoryChart } from './generic-charts' | ||||
| import { TooltipProps, MARGIN_X, MARGIN_Y } from './helpers' | ||||
| import { SizedContainer } from 'web/components/sized-container' | ||||
| 
 | ||||
| const getPoints = (startDate: number, dailyValues: number[]) => { | ||||
|   const startDateDayJs = dayjs(startDate) | ||||
|   return dailyValues.map((y, i) => ({ | ||||
|     x: startDateDayJs.add(i, 'day').toDate(), | ||||
|     y: y, | ||||
|   })) | ||||
| } | ||||
| 
 | ||||
| const DailyCountTooltip = (props: TooltipProps<Date, HistoryPoint>) => { | ||||
|   const { data, mouseX, xScale } = props | ||||
|   const d = xScale.invert(mouseX) | ||||
|   return ( | ||||
|     <Row className="items-center gap-2"> | ||||
|       <span className="font-semibold">{dayjs(d).format('MMM DD')}</span> | ||||
|       <span className="text-greyscale-6">{data.y}</span> | ||||
|     </Row> | ||||
|   ) | ||||
| } | ||||
| 
 | ||||
| const DailyPercentTooltip = (props: TooltipProps<Date, HistoryPoint>) => { | ||||
|   const { data, mouseX, xScale } = props | ||||
|   const d = xScale.invert(mouseX) | ||||
|   return ( | ||||
|     <Row className="items-center gap-2"> | ||||
|       <span className="font-semibold">{dayjs(d).format('MMM DD')}</span> | ||||
|       <span className="text-greyscale-6">{formatPercent(data.y)}</span> | ||||
|     </Row> | ||||
|   ) | ||||
| } | ||||
| 
 | ||||
| export function DailyChart(props: { | ||||
|   startDate: number | ||||
|   dailyValues: number[] | ||||
|   excludeFirstDays?: number | ||||
|   pct?: boolean | ||||
| }) { | ||||
|   const { dailyValues, startDate, excludeFirstDays, pct } = props | ||||
| 
 | ||||
|   const data = useMemo( | ||||
|     () => getPoints(startDate, dailyValues).slice(excludeFirstDays ?? 0), | ||||
|     [startDate, dailyValues, excludeFirstDays] | ||||
|   ) | ||||
|   // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
 | ||||
|   const minDate = min(data.map((d) => d.x))! | ||||
|   // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
 | ||||
|   const maxDate = max(data.map((d) => d.x))! | ||||
|   // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
 | ||||
|   const maxValue = max(data.map((d) => d.y))! | ||||
|   return ( | ||||
|     <SizedContainer fullHeight={250} mobileHeight={250}> | ||||
|       {(width, height) => ( | ||||
|         <SingleValueHistoryChart | ||||
|           w={width} | ||||
|           h={height} | ||||
|           xScale={scaleTime([minDate, maxDate], [0, width - MARGIN_X])} | ||||
|           yScale={scaleLinear([0, maxValue], [height - MARGIN_Y, 0])} | ||||
|           data={data} | ||||
|           Tooltip={pct ? DailyPercentTooltip : DailyCountTooltip} | ||||
|           color="#11b981" | ||||
|           pct={pct} | ||||
|         /> | ||||
|       )} | ||||
|     </SizedContainer> | ||||
|   ) | ||||
| } | ||||
|  | @ -393,7 +393,9 @@ export function ContractCardProbChange(props: { | |||
|   noLinkAvatar?: boolean | ||||
|   className?: string | ||||
| }) { | ||||
|   const { contract, noLinkAvatar, className } = props | ||||
|   const { noLinkAvatar, className } = props | ||||
|   const contract = useContractWithPreload(props.contract) as CPMMBinaryContract | ||||
| 
 | ||||
|   return ( | ||||
|     <Col | ||||
|       className={clsx( | ||||
|  |  | |||
|  | @ -1,5 +1,3 @@ | |||
| import React, { useEffect, useRef, useState } from 'react' | ||||
| 
 | ||||
| import { tradingAllowed } from 'web/lib/firebase/contracts' | ||||
| import { Col } from '../layout/col' | ||||
| import { ContractChart } from 'web/components/charts/contract' | ||||
|  | @ -24,6 +22,7 @@ import { | |||
|   BinaryContract, | ||||
| } from 'common/contract' | ||||
| import { ContractDetails } from './contract-details' | ||||
| import { SizedContainer } from 'web/components/sized-container' | ||||
| 
 | ||||
| const OverviewQuestion = (props: { text: string }) => ( | ||||
|   <Linkify className="text-lg text-indigo-700 sm:text-2xl" text={props.text} /> | ||||
|  | @ -49,32 +48,18 @@ const SizedContractChart = (props: { | |||
|   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]) | ||||
|   const { fullHeight, mobileHeight, contract, bets } = props | ||||
|   return ( | ||||
|     <div ref={containerRef}> | ||||
|       {chartWidth != null && chartHeight != null && ( | ||||
|     <SizedContainer fullHeight={fullHeight} mobileHeight={mobileHeight}> | ||||
|       {(width, height) => ( | ||||
|         <ContractChart | ||||
|           width={width} | ||||
|           height={height} | ||||
|           contract={contract} | ||||
|           bets={bets} | ||||
|           width={chartWidth} | ||||
|           height={chartHeight} | ||||
|         /> | ||||
|       )} | ||||
|     </div> | ||||
|     </SizedContainer> | ||||
|   ) | ||||
| } | ||||
| 
 | ||||
|  | @ -114,7 +99,11 @@ const BinaryOverview = (props: { contract: BinaryContract; bets: Bet[] }) => { | |||
|         <ContractDetails contract={contract} /> | ||||
|         <Row className="justify-between gap-4"> | ||||
|           <OverviewQuestion text={contract.question} /> | ||||
|           <BinaryResolutionOrChance contract={contract} large /> | ||||
|           <BinaryResolutionOrChance | ||||
|             className="flex items-end" | ||||
|             contract={contract} | ||||
|             large | ||||
|           /> | ||||
|         </Row> | ||||
|       </Col> | ||||
|       <SizedContractChart | ||||
|  |  | |||
|  | @ -77,13 +77,34 @@ const CommentsTabContent = memo(function CommentsTabContent(props: { | |||
|   const comments = useComments(contract.id) ?? props.comments | ||||
|   const [sort, setSort] = useState<'Newest' | 'Best'>('Newest') | ||||
|   const me = useUser() | ||||
| 
 | ||||
|   if (comments == null) { | ||||
|     return <LoadingIndicator /> | ||||
|   } | ||||
| 
 | ||||
|   const tipsOrBountiesAwarded = | ||||
|     Object.keys(tips).length > 0 || comments.some((c) => c.bountiesAwarded) | ||||
| 
 | ||||
|   const sortedComments = sortBy(comments, (c) => | ||||
|     sort === 'Newest' | ||||
|       ? c.createdTime | ||||
|       : // Is this too magic? If there are tips/bounties, 'Best' shows your own comments made within the last 10 minutes first, then sorts by score
 | ||||
|       tipsOrBountiesAwarded && | ||||
|         c.createdTime > Date.now() - 10 * MINUTE_MS && | ||||
|         c.userId === me?.id | ||||
|       ? -Infinity | ||||
|       : -((c.bountiesAwarded ?? 0) + sum(Object.values(tips[c.id] ?? []))) | ||||
|   ) | ||||
| 
 | ||||
|   const commentsByParent = groupBy( | ||||
|     sortedComments, | ||||
|     (c) => c.replyToCommentId ?? '_' | ||||
|   ) | ||||
|   const topLevelComments = commentsByParent['_'] ?? [] | ||||
|   // Top level comments are reverse-chronological, while replies are chronological
 | ||||
|   if (sort === 'Newest') topLevelComments.reverse() | ||||
| 
 | ||||
|   if (contract.outcomeType === 'FREE_RESPONSE') { | ||||
|     const generalComments = comments.filter( | ||||
|       (c) => c.answerOutcome === undefined && c.betId === undefined | ||||
|     ) | ||||
|     const sortedAnswers = sortBy( | ||||
|       contract.answers, | ||||
|       (a) => -getOutcomeProbability(contract, a.id) | ||||
|  | @ -92,6 +113,9 @@ const CommentsTabContent = memo(function CommentsTabContent(props: { | |||
|       comments, | ||||
|       (c) => c.answerOutcome ?? c.betOutcome ?? '_' | ||||
|     ) | ||||
|     const generalTopLevelComments = topLevelComments.filter( | ||||
|       (c) => c.answerOutcome === undefined && c.betId === undefined | ||||
|     ) | ||||
|     return ( | ||||
|       <> | ||||
|         {sortedAnswers.map((answer) => ( | ||||
|  | @ -115,12 +139,12 @@ const CommentsTabContent = memo(function CommentsTabContent(props: { | |||
|           <div className="text-md mt-8 mb-2 text-left">General Comments</div> | ||||
|           <div className="mb-4 w-full border-b border-gray-200" /> | ||||
|           <ContractCommentInput className="mb-5" contract={contract} /> | ||||
|           {generalComments.map((comment) => ( | ||||
|           {generalTopLevelComments.map((comment) => ( | ||||
|             <FeedCommentThread | ||||
|               key={comment.id} | ||||
|               contract={contract} | ||||
|               parentComment={comment} | ||||
|               threadComments={[]} | ||||
|               threadComments={commentsByParent[comment.id] ?? []} | ||||
|               tips={tips} | ||||
|             /> | ||||
|           ))} | ||||
|  | @ -128,24 +152,6 @@ const CommentsTabContent = memo(function CommentsTabContent(props: { | |||
|       </> | ||||
|     ) | ||||
|   } else { | ||||
|     const tipsOrBountiesAwarded = | ||||
|       Object.keys(tips).length > 0 || comments.some((c) => c.bountiesAwarded) | ||||
| 
 | ||||
|     const commentsByParent = groupBy( | ||||
|       sortBy(comments, (c) => | ||||
|         sort === 'Newest' | ||||
|           ? -c.createdTime | ||||
|           : // Is this too magic? If there are tips/bounties, 'Best' shows your own comments made within the last 10 minutes first, then sorts by score
 | ||||
|           tipsOrBountiesAwarded && | ||||
|             c.createdTime > Date.now() - 10 * MINUTE_MS && | ||||
|             c.userId === me?.id | ||||
|           ? -Infinity | ||||
|           : -((c.bountiesAwarded ?? 0) + sum(Object.values(tips[c.id] ?? []))) | ||||
|       ), | ||||
|       (c) => c.replyToCommentId ?? '_' | ||||
|     ) | ||||
| 
 | ||||
|     const topLevelComments = commentsByParent['_'] ?? [] | ||||
|     return ( | ||||
|       <> | ||||
|         <ContractCommentInput className="mb-5" contract={contract} /> | ||||
|  |  | |||
							
								
								
									
										35
									
								
								web/components/sized-container.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								web/components/sized-container.tsx
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,35 @@ | |||
| import { ReactNode, useEffect, useRef, useState } from 'react' | ||||
| 
 | ||||
| export const SizedContainer = (props: { | ||||
|   fullHeight: number | ||||
|   mobileHeight: number | ||||
|   mobileThreshold?: number | ||||
|   children: (width: number, height: number) => ReactNode | ||||
| }) => { | ||||
|   const { children, fullHeight, mobileHeight } = props | ||||
|   const threshold = props.mobileThreshold ?? 800 | ||||
|   const containerRef = useRef<HTMLDivElement>(null) | ||||
|   const [width, setWidth] = useState<number>() | ||||
|   const [height, setHeight] = useState<number>() | ||||
|   useEffect(() => { | ||||
|     if (containerRef.current) { | ||||
|       const handleResize = () => { | ||||
|         setHeight(window.innerWidth <= threshold ? mobileHeight : fullHeight) | ||||
|         setWidth(containerRef.current?.clientWidth) | ||||
|       } | ||||
|       handleResize() | ||||
|       const resizeObserver = new ResizeObserver(handleResize) | ||||
|       resizeObserver.observe(containerRef.current) | ||||
|       window.addEventListener('resize', handleResize) | ||||
|       return () => { | ||||
|         window.removeEventListener('resize', handleResize) | ||||
|         resizeObserver.disconnect() | ||||
|       } | ||||
|     } | ||||
|   }, [threshold, fullHeight, mobileHeight]) | ||||
|   return ( | ||||
|     <div ref={containerRef}> | ||||
|       {width != null && height != null && children(width, height)} | ||||
|     </div> | ||||
|   ) | ||||
| } | ||||
|  | @ -11,7 +11,7 @@ const App = () => { | |||
|         url="/cowp" | ||||
|       /> | ||||
|       <Link href="https://www.youtube.com/watch?v=FavUpD_IjVY"> | ||||
|         <img src="https://i.imgur.com/Lt54IiU.png" /> | ||||
|         <img src="https://i.imgur.com/Lt54IiU.png" className="cursor-pointer" /> | ||||
|       </Link> | ||||
|     </Page> | ||||
|   ) | ||||
|  |  | |||
|  | @ -16,7 +16,7 @@ export default function LabsPage() { | |||
|         url="/labs" | ||||
|       /> | ||||
|       <Col className="px-4"> | ||||
|         <Title className="sm:!mt-0" text="Manifold Labs" /> | ||||
|         <Title className="sm:!mt-0" text="🧪 Manifold Labs" /> | ||||
| 
 | ||||
|         <Masonry | ||||
|           breakpointCols={{ default: 2, 768: 1 }} | ||||
|  |  | |||
|  | @ -1,8 +1,5 @@ | |||
| import { useEffect, useState } from 'react' | ||||
| import { | ||||
|   DailyCountChart, | ||||
|   DailyPercentChart, | ||||
| } from 'web/components/analytics/charts' | ||||
| import { DailyChart } from 'web/components/charts/stats' | ||||
| import { Col } from 'web/components/layout/col' | ||||
| import { Spacer } from 'web/components/layout/spacer' | ||||
| import { Tabs } from 'web/components/layout/tabs' | ||||
|  | @ -96,40 +93,36 @@ export function CustomAnalytics(props: Stats) { | |||
|           { | ||||
|             title: 'Daily', | ||||
|             content: ( | ||||
|               <DailyCountChart | ||||
|                 dailyCounts={dailyActiveUsers} | ||||
|               <DailyChart | ||||
|                 dailyValues={dailyActiveUsers} | ||||
|                 startDate={startDate} | ||||
|                 small | ||||
|               /> | ||||
|             ), | ||||
|           }, | ||||
|           { | ||||
|             title: 'Daily (7d avg)', | ||||
|             content: ( | ||||
|               <DailyCountChart | ||||
|                 dailyCounts={dailyActiveUsersWeeklyAvg} | ||||
|               <DailyChart | ||||
|                 dailyValues={dailyActiveUsersWeeklyAvg} | ||||
|                 startDate={startDate} | ||||
|                 small | ||||
|               /> | ||||
|             ), | ||||
|           }, | ||||
|           { | ||||
|             title: 'Weekly', | ||||
|             content: ( | ||||
|               <DailyCountChart | ||||
|                 dailyCounts={weeklyActiveUsers} | ||||
|               <DailyChart | ||||
|                 dailyValues={weeklyActiveUsers} | ||||
|                 startDate={startDate} | ||||
|                 small | ||||
|               /> | ||||
|             ), | ||||
|           }, | ||||
|           { | ||||
|             title: 'Monthly', | ||||
|             content: ( | ||||
|               <DailyCountChart | ||||
|                 dailyCounts={monthlyActiveUsers} | ||||
|               <DailyChart | ||||
|                 dailyValues={monthlyActiveUsers} | ||||
|                 startDate={startDate} | ||||
|                 small | ||||
|               /> | ||||
|             ), | ||||
|           }, | ||||
|  | @ -149,44 +142,44 @@ export function CustomAnalytics(props: Stats) { | |||
|           { | ||||
|             title: 'D1', | ||||
|             content: ( | ||||
|               <DailyPercentChart | ||||
|                 dailyPercent={d1} | ||||
|               <DailyChart | ||||
|                 dailyValues={d1} | ||||
|                 startDate={startDate} | ||||
|                 small | ||||
|                 excludeFirstDays={1} | ||||
|                 pct | ||||
|               /> | ||||
|             ), | ||||
|           }, | ||||
|           { | ||||
|             title: 'D1 (7d avg)', | ||||
|             content: ( | ||||
|               <DailyPercentChart | ||||
|                 dailyPercent={d1WeeklyAvg} | ||||
|               <DailyChart | ||||
|                 dailyValues={d1WeeklyAvg} | ||||
|                 startDate={startDate} | ||||
|                 small | ||||
|                 excludeFirstDays={7} | ||||
|                 pct | ||||
|               /> | ||||
|             ), | ||||
|           }, | ||||
|           { | ||||
|             title: 'W1', | ||||
|             content: ( | ||||
|               <DailyPercentChart | ||||
|                 dailyPercent={weekOnWeekRetention} | ||||
|               <DailyChart | ||||
|                 dailyValues={weekOnWeekRetention} | ||||
|                 startDate={startDate} | ||||
|                 small | ||||
|                 excludeFirstDays={14} | ||||
|                 pct | ||||
|               /> | ||||
|             ), | ||||
|           }, | ||||
|           { | ||||
|             title: 'M1', | ||||
|             content: ( | ||||
|               <DailyPercentChart | ||||
|                 dailyPercent={monthlyRetention} | ||||
|               <DailyChart | ||||
|                 dailyValues={monthlyRetention} | ||||
|                 startDate={startDate} | ||||
|                 small | ||||
|                 excludeFirstDays={60} | ||||
|                 pct | ||||
|               /> | ||||
|             ), | ||||
|           }, | ||||
|  | @ -207,33 +200,33 @@ export function CustomAnalytics(props: Stats) { | |||
|           { | ||||
|             title: 'ND1', | ||||
|             content: ( | ||||
|               <DailyPercentChart | ||||
|                 dailyPercent={nd1} | ||||
|               <DailyChart | ||||
|                 dailyValues={nd1} | ||||
|                 startDate={startDate} | ||||
|                 excludeFirstDays={1} | ||||
|                 small | ||||
|                 pct | ||||
|               /> | ||||
|             ), | ||||
|           }, | ||||
|           { | ||||
|             title: 'ND1 (7d avg)', | ||||
|             content: ( | ||||
|               <DailyPercentChart | ||||
|                 dailyPercent={nd1WeeklyAvg} | ||||
|               <DailyChart | ||||
|                 dailyValues={nd1WeeklyAvg} | ||||
|                 startDate={startDate} | ||||
|                 excludeFirstDays={7} | ||||
|                 small | ||||
|                 pct | ||||
|               /> | ||||
|             ), | ||||
|           }, | ||||
|           { | ||||
|             title: 'NW1', | ||||
|             content: ( | ||||
|               <DailyPercentChart | ||||
|                 dailyPercent={nw1} | ||||
|               <DailyChart | ||||
|                 dailyValues={nw1} | ||||
|                 startDate={startDate} | ||||
|                 excludeFirstDays={14} | ||||
|                 small | ||||
|                 pct | ||||
|               /> | ||||
|             ), | ||||
|           }, | ||||
|  | @ -249,41 +242,31 @@ export function CustomAnalytics(props: Stats) { | |||
|           { | ||||
|             title: capitalize(PAST_BETS), | ||||
|             content: ( | ||||
|               <DailyCountChart | ||||
|                 dailyCounts={dailyBetCounts} | ||||
|                 startDate={startDate} | ||||
|                 small | ||||
|               /> | ||||
|               <DailyChart dailyValues={dailyBetCounts} startDate={startDate} /> | ||||
|             ), | ||||
|           }, | ||||
|           { | ||||
|             title: 'Markets created', | ||||
|             content: ( | ||||
|               <DailyCountChart | ||||
|                 dailyCounts={dailyContractCounts} | ||||
|               <DailyChart | ||||
|                 dailyValues={dailyContractCounts} | ||||
|                 startDate={startDate} | ||||
|                 small | ||||
|               /> | ||||
|             ), | ||||
|           }, | ||||
|           { | ||||
|             title: 'Comments', | ||||
|             content: ( | ||||
|               <DailyCountChart | ||||
|                 dailyCounts={dailyCommentCounts} | ||||
|               <DailyChart | ||||
|                 dailyValues={dailyCommentCounts} | ||||
|                 startDate={startDate} | ||||
|                 small | ||||
|               /> | ||||
|             ), | ||||
|           }, | ||||
|           { | ||||
|             title: 'Signups', | ||||
|             content: ( | ||||
|               <DailyCountChart | ||||
|                 dailyCounts={dailySignups} | ||||
|                 startDate={startDate} | ||||
|                 small | ||||
|               /> | ||||
|               <DailyChart dailyValues={dailySignups} startDate={startDate} /> | ||||
|             ), | ||||
|           }, | ||||
|         ]} | ||||
|  | @ -304,22 +287,22 @@ export function CustomAnalytics(props: Stats) { | |||
|           { | ||||
|             title: 'Daily', | ||||
|             content: ( | ||||
|               <DailyPercentChart | ||||
|                 dailyPercent={dailyActivationRate} | ||||
|               <DailyChart | ||||
|                 dailyValues={dailyActivationRate} | ||||
|                 startDate={startDate} | ||||
|                 excludeFirstDays={1} | ||||
|                 small | ||||
|                 pct | ||||
|               /> | ||||
|             ), | ||||
|           }, | ||||
|           { | ||||
|             title: 'Daily (7d avg)', | ||||
|             content: ( | ||||
|               <DailyPercentChart | ||||
|                 dailyPercent={dailyActivationRateWeeklyAvg} | ||||
|               <DailyChart | ||||
|                 dailyValues={dailyActivationRateWeeklyAvg} | ||||
|                 startDate={startDate} | ||||
|                 excludeFirstDays={7} | ||||
|                 small | ||||
|                 pct | ||||
|               /> | ||||
|             ), | ||||
|           }, | ||||
|  | @ -335,33 +318,33 @@ export function CustomAnalytics(props: Stats) { | |||
|           { | ||||
|             title: 'Daily / Weekly', | ||||
|             content: ( | ||||
|               <DailyPercentChart | ||||
|                 dailyPercent={dailyDividedByWeekly} | ||||
|               <DailyChart | ||||
|                 dailyValues={dailyDividedByWeekly} | ||||
|                 startDate={startDate} | ||||
|                 small | ||||
|                 excludeFirstDays={7} | ||||
|                 pct | ||||
|               /> | ||||
|             ), | ||||
|           }, | ||||
|           { | ||||
|             title: 'Daily / Monthly', | ||||
|             content: ( | ||||
|               <DailyPercentChart | ||||
|                 dailyPercent={dailyDividedByMonthly} | ||||
|               <DailyChart | ||||
|                 dailyValues={dailyDividedByMonthly} | ||||
|                 startDate={startDate} | ||||
|                 small | ||||
|                 excludeFirstDays={30} | ||||
|                 pct | ||||
|               /> | ||||
|             ), | ||||
|           }, | ||||
|           { | ||||
|             title: 'Weekly / Monthly', | ||||
|             content: ( | ||||
|               <DailyPercentChart | ||||
|                 dailyPercent={weeklyDividedByMonthly} | ||||
|               <DailyChart | ||||
|                 dailyValues={weeklyDividedByMonthly} | ||||
|                 startDate={startDate} | ||||
|                 small | ||||
|                 excludeFirstDays={30} | ||||
|                 pct | ||||
|               /> | ||||
|             ), | ||||
|           }, | ||||
|  | @ -380,31 +363,19 @@ export function CustomAnalytics(props: Stats) { | |||
|           { | ||||
|             title: 'Daily', | ||||
|             content: ( | ||||
|               <DailyCountChart | ||||
|                 dailyCounts={manaBet.daily} | ||||
|                 startDate={startDate} | ||||
|                 small | ||||
|               /> | ||||
|               <DailyChart dailyValues={manaBet.daily} startDate={startDate} /> | ||||
|             ), | ||||
|           }, | ||||
|           { | ||||
|             title: 'Weekly', | ||||
|             content: ( | ||||
|               <DailyCountChart | ||||
|                 dailyCounts={manaBet.weekly} | ||||
|                 startDate={startDate} | ||||
|                 small | ||||
|               /> | ||||
|               <DailyChart dailyValues={manaBet.weekly} startDate={startDate} /> | ||||
|             ), | ||||
|           }, | ||||
|           { | ||||
|             title: 'Monthly', | ||||
|             content: ( | ||||
|               <DailyCountChart | ||||
|                 dailyCounts={manaBet.monthly} | ||||
|                 startDate={startDate} | ||||
|                 small | ||||
|               /> | ||||
|               <DailyChart dailyValues={manaBet.monthly} startDate={startDate} /> | ||||
|             ), | ||||
|           }, | ||||
|         ]} | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue
	
	Block a user