From 77a5f8b9dda28aa7fca386d1928eadd596f38058 Mon Sep 17 00:00:00 2001 From: Pico2x Date: Mon, 3 Oct 2022 17:31:07 +0100 Subject: [PATCH] Revert the merge revert (double revert) --- web/components/analytics/charts.tsx | 139 ------------------ web/components/charts/contract/binary.tsx | 2 + web/components/charts/contract/choice.tsx | 2 + .../charts/contract/pseudo-numeric.tsx | 8 +- web/components/charts/generic-charts.tsx | 19 ++- web/components/charts/helpers.tsx | 12 +- web/components/charts/stats.tsx | 76 ++++++++++ web/components/contract/contract-card.tsx | 4 +- web/components/contract/contract-overview.tsx | 29 +--- web/components/contract/contract-tabs.tsx | 34 ++++- web/components/sized-container.tsx | 35 +++++ web/pages/cowp.tsx | 2 +- web/pages/labs/index.tsx | 2 +- web/pages/stats.tsx | 137 +++++++---------- 14 files changed, 232 insertions(+), 269 deletions(-) delete mode 100644 web/components/analytics/charts.tsx create mode 100644 web/components/charts/stats.tsx create mode 100644 web/components/sized-container.tsx diff --git a/web/components/analytics/charts.tsx b/web/components/analytics/charts.tsx deleted file mode 100644 index 131ce2a0..00000000 --- a/web/components/analytics/charts.tsx +++ /dev/null @@ -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 ( -
- 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 - }} - /> -
- ) -} - -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 ( -
- 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 - }} - /> -
- ) -} - -function Tooltip(props: { point: Point; isPercent?: boolean }) { - const { point, isPercent } = props - return ( - -
- {point.serieId}{' '} - {isPercent ? formatPercent(+point.data.y) : Math.round(+point.data.y)} -
-
{dayjs(point.data.x).format('MMM DD')}
- - ) -} diff --git a/web/components/charts/contract/binary.tsx b/web/components/charts/contract/binary.tsx index 7e192767..c9b3bb0b 100644 --- a/web/components/charts/contract/binary.tsx +++ b/web/components/charts/contract/binary.tsx @@ -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 diff --git a/web/components/charts/contract/choice.tsx b/web/components/charts/contract/choice.tsx index 65279b70..99e02fa8 100644 --- a/web/components/charts/contract/choice.tsx +++ b/web/components/charts/contract/choice.tsx @@ -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 diff --git a/web/components/charts/contract/pseudo-numeric.tsx b/web/components/charts/contract/pseudo-numeric.tsx index e03d4ad9..e3edb11f 100644 --- a/web/components/charts/contract/pseudo-numeric.tsx +++ b/web/components/charts/contract/pseudo-numeric.tsx @@ -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 ( (props: { color: string xScale: ScaleContinuousNumeric yScale: ScaleContinuousNumeric + curve?: CurveFactory onMouseOver?: (p: P | undefined) => void Tooltip?: TooltipComponent }) => { - const { color, data, yScale, w, h, Tooltip } = props + const { color, data, yScale, w, h, curve, Tooltip } = props const [viewXScale, setViewXScale] = useState>() @@ -100,7 +101,7 @@ export const DistributionChart =

(props: { px={px} py0={py0} py1={py1} - curve={curveLinear} + curve={curve ?? curveLinear} /> ) @@ -113,11 +114,12 @@ export const MultiValueHistoryChart =

(props: { colors: readonly string[] xScale: ScaleTime yScale: ScaleContinuousNumeric + curve?: CurveFactory onMouseOver?: (p: P | undefined) => void Tooltip?: TooltipComponent 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>() const xScale = viewXScale ?? props.xScale @@ -177,7 +179,7 @@ export const MultiValueHistoryChart =

(props: { px={px} py0={py0} py1={py1} - curve={curveStepAfter} + curve={curve ?? curveLinear} fill={colors[i]} /> ))} @@ -192,11 +194,12 @@ export const SingleValueHistoryChart =

(props: { color: string xScale: ScaleTime yScale: ScaleContinuousNumeric + curve?: CurveFactory onMouseOver?: (p: P | undefined) => void Tooltip?: TooltipComponent 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>() const xScale = viewXScale ?? props.xScale @@ -246,7 +249,7 @@ export const SingleValueHistoryChart =

(props: { px={px} py0={py0} py1={py1} - curve={curveStepAfter} + curve={curve ?? curveLinear} /> ) diff --git a/web/components/charts/helpers.tsx b/web/components/charts/helpers.tsx index 96115dc0..b40ab7db 100644 --- a/web/components/charts/helpers.tsx +++ b/web/components/charts/helpers.tsx @@ -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 = ( data: P[] px: number | ((p: P) => number) py: number | ((p: P) => number) - curve?: CurveFactory + curve: CurveFactory } & SVGProps ) => { const { data, px, py, curve, ...rest } = props - const d3Line = line

(px, py).curve(curve ?? curveStepAfter) + const d3Line = line

(px, py).curve(curve) // eslint-disable-next-line @typescript-eslint/no-non-null-assertion return } @@ -89,11 +89,11 @@ const AreaPathInternal = ( px: number | ((p: P) => number) py0: number | ((p: P) => number) py1: number | ((p: P) => number) - curve?: CurveFactory + curve: CurveFactory } & SVGProps ) => { const { data, px, py0, py1, curve, ...rest } = props - const d3Area = area

(px, py0, py1).curve(curve ?? curveStepAfter) + const d3Area = area

(px, py0, py1).curve(curve) // eslint-disable-next-line @typescript-eslint/no-non-null-assertion return } @@ -105,7 +105,7 @@ export const AreaWithTopStroke = (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 ( diff --git a/web/components/charts/stats.tsx b/web/components/charts/stats.tsx new file mode 100644 index 00000000..a630657a --- /dev/null +++ b/web/components/charts/stats.tsx @@ -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) => { + const { data, mouseX, xScale } = props + const d = xScale.invert(mouseX) + return ( + + {dayjs(d).format('MMM DD')} + {data.y} + + ) +} + +const DailyPercentTooltip = (props: TooltipProps) => { + const { data, mouseX, xScale } = props + const d = xScale.invert(mouseX) + return ( + + {dayjs(d).format('MMM DD')} + {formatPercent(data.y)} + + ) +} + +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 ( + + {(width, height) => ( + + )} + + ) +} diff --git a/web/components/contract/contract-card.tsx b/web/components/contract/contract-card.tsx index b783b4e0..401005bf 100644 --- a/web/components/contract/contract-card.tsx +++ b/web/components/contract/contract-card.tsx @@ -395,7 +395,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 ( ( @@ -50,32 +49,18 @@ const SizedContractChart = (props: { 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]) + const { fullHeight, mobileHeight, contract, bets } = props return ( -

- {chartWidth != null && chartHeight != null && ( + + {(width, height) => ( )} -
+ ) } diff --git a/web/components/contract/contract-tabs.tsx b/web/components/contract/contract-tabs.tsx index c4fddca5..c28cb5cb 100644 --- a/web/components/contract/contract-tabs.tsx +++ b/web/components/contract/contract-tabs.tsx @@ -85,13 +85,34 @@ const CommentsTabContent = memo(function CommentsTabContent(props: { store: storageStore(safeLocalStorage()), }) const me = useUser() + if (comments == null) { return } + + 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) @@ -100,6 +121,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) => ( @@ -123,12 +147,12 @@ const CommentsTabContent = memo(function CommentsTabContent(props: {
General Comments
- {generalComments.map((comment) => ( + {generalTopLevelComments.map((comment) => ( ))} diff --git a/web/components/sized-container.tsx b/web/components/sized-container.tsx new file mode 100644 index 00000000..26532047 --- /dev/null +++ b/web/components/sized-container.tsx @@ -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(null) + const [width, setWidth] = useState() + const [height, setHeight] = useState() + 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 ( +
+ {width != null && height != null && children(width, height)} +
+ ) +} diff --git a/web/pages/cowp.tsx b/web/pages/cowp.tsx index 21494c37..a854f141 100644 --- a/web/pages/cowp.tsx +++ b/web/pages/cowp.tsx @@ -11,7 +11,7 @@ const App = () => { url="/cowp" /> - + ) diff --git a/web/pages/labs/index.tsx b/web/pages/labs/index.tsx index bd1dbb35..79f44a64 100644 --- a/web/pages/labs/index.tsx +++ b/web/pages/labs/index.tsx @@ -16,7 +16,7 @@ export default function LabsPage() { url="/labs" /> - + <Title className="sm:!mt-0" text="🧪 Manifold Labs" /> <Masonry breakpointCols={{ default: 2, 768: 1 }} diff --git a/web/pages/stats.tsx b/web/pages/stats.tsx index 19fab509..125af4bd 100644 --- a/web/pages/stats.tsx +++ b/web/pages/stats.tsx @@ -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} /> ), }, ]}