diff --git a/src/components/visx/TimeSeriesChart.tsx b/src/components/visx/TimeSeriesChart.tsx index ffb85feab..e20070123 100644 --- a/src/components/visx/TimeSeriesChart.tsx +++ b/src/components/visx/TimeSeriesChart.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useMemo, useRef, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { LinearGradient } from '@visx/gradient'; import { ParentSize } from '@visx/responsive'; @@ -141,17 +141,32 @@ export const TimeSeriesChart = ({ const latestDatum = data?.[data.length - 1]; // Chart state + const getClampedZoomDomain = useCallback( + (unclamped: number) => { + return clamp( + Math.max(1e-320, Math.min(Number.MAX_SAFE_INTEGER, unclamped)), + minZoomDomain, + xAccessor(latestDatum) - xAccessor(earliestDatum) + ); + }, + [earliestDatum, latestDatum, minZoomDomain, xAccessor] + ); + const [zoomDomain, setZoomDomain] = useState( - defaultZoomDomain ?? xAccessor(latestDatum) - xAccessor(earliestDatum) + defaultZoomDomain + ? getClampedZoomDomain(defaultZoomDomain) + : xAccessor(latestDatum) - xAccessor(earliestDatum) ); const [zoomDomainAnimateTo, setZoomDomainAnimateTo] = useState(); useEffect(() => { if (defaultZoomDomain) { - setZoomDomainAnimateTo(defaultZoomDomain); + const clampedZoomDomain = getClampedZoomDomain(defaultZoomDomain); + setZoomDomain(clampedZoomDomain); + setZoomDomainAnimateTo(clampedZoomDomain); } - }, [defaultZoomDomain]); + }, [defaultZoomDomain, getClampedZoomDomain]); useEffect(() => { onZoom?.({ zoomDomain }); @@ -219,14 +234,7 @@ export const TimeSeriesChart = ({ const onWheel = ({ deltaY }: React.WheelEvent) => { if (!zoomDomain) return; - setZoomDomain( - clamp( - Math.max(1e-320, Math.min(Number.MAX_SAFE_INTEGER, zoomDomain * Math.exp(deltaY / 1000))), - minZoomDomain, - xAccessor(latestDatum) - xAccessor(earliestDatum) - ) - ); - + setZoomDomain(getClampedZoomDomain(zoomDomain * Math.exp(deltaY / 1000))); setZoomDomainAnimateTo(undefined); // TODO: scroll horizontally to pan diff --git a/src/pages/token/rewards/RewardHistoryPanel.tsx b/src/pages/token/rewards/RewardHistoryPanel.tsx index 17feb802e..8672e9221 100644 --- a/src/pages/token/rewards/RewardHistoryPanel.tsx +++ b/src/pages/token/rewards/RewardHistoryPanel.tsx @@ -1,115 +1,73 @@ -import { useCallback, useState } from 'react'; - import styled from 'styled-components'; -import { - HISTORICAL_TRADING_REWARDS_PERIODS, - HistoricalTradingRewardsPeriod, - HistoricalTradingRewardsPeriods, -} from '@/constants/abacus'; +import { HistoricalTradingRewardsPeriod } from '@/constants/abacus'; import { STRING_KEYS } from '@/constants/localization'; import { useEnvConfig } from '@/hooks/useEnvConfig'; import { useStringGetter } from '@/hooks/useStringGetter'; import breakpoints from '@/styles/breakpoints'; -import { layoutMixins } from '@/styles/layoutMixins'; import { Output, OutputType } from '@/components/Output'; -import { Panel } from '@/components/Panel'; -import { ToggleGroup } from '@/components/ToggleGroup'; -import { WithTooltip } from '@/components/WithTooltip'; import { TradingRewardHistoryTable } from '@/views/tables/TradingRewardHistoryTable'; -import abacusStateManager from '@/lib/abacus'; - export const RewardHistoryPanel = () => { const stringGetter = useStringGetter(); const rewardsHistoryStartDate = useEnvConfig('rewardsHistoryStartDateMs'); - const [selectedPeriod, setSelectedPeriod] = useState( - abacusStateManager.getHistoricalTradingRewardPeriod() || HistoricalTradingRewardsPeriod.WEEKLY - ); - - const onSelectPeriod = useCallback((periodName: string) => { - const thisSelectedPeriod = - HISTORICAL_TRADING_REWARDS_PERIODS[ - periodName as keyof typeof HISTORICAL_TRADING_REWARDS_PERIODS - ]; - setSelectedPeriod(thisSelectedPeriod); - abacusStateManager.setHistoricalTradingRewardPeriod(thisSelectedPeriod); - }, []); - return ( - - <$Title> - -

{stringGetter({ key: STRING_KEYS.REWARD_HISTORY })}

-
- - {stringGetter({ - key: STRING_KEYS.REWARD_HISTORY_DESCRIPTION, - params: { - REWARDS_HISTORY_START_DATE: ( - <$Output - type={OutputType.Date} - value={Number(rewardsHistoryStartDate)} - timeOptions={{ useUTC: true }} - /> - ), - }, - })} - - - - - } - > - -
+ <$RewardHistoryContainer> + <$Header> + <$Title>{stringGetter({ key: STRING_KEYS.TRADING_REWARD_HISTORY })} + + <$TradingRewardHistoryTable period={HistoricalTradingRewardsPeriod.DAILY} /> + <$Description> + {stringGetter({ + key: STRING_KEYS.REWARD_HISTORY_DESCRIPTION, + params: { + REWARDS_HISTORY_START_DATE: ( + <$Output + type={OutputType.Date} + value={Number(rewardsHistoryStartDate)} + timeOptions={{ useUTC: true }} + /> + ), + }, + })} + + ); }; -const $Header = styled.div` - ${layoutMixins.spacedRow} - padding: 1rem 1rem 0; - margin-bottom: -0.5rem; +const $RewardHistoryContainer = styled.div` + display: flex; + flex-direction: column; + gap: var(--gap); +`; +const $Header = styled.div` @media ${breakpoints.notTablet} { - padding: 1.25rem 1.5rem 0; + margin-bottom: -0.5rem; } `; const $Title = styled.div` - ${layoutMixins.column} - color: var(--color-text-0); - font: var(--font-small-book); + font: var(--font-large-book); + color: var(--color-text-2); +`; - h3 { - font: var(--font-medium-book); - color: var(--color-text-2); - } +const $TradingRewardHistoryTable = styled(TradingRewardHistoryTable)` + --computed-radius: 0.875rem; `; const $Output = styled(Output)` display: inline; `; + +const $Description = styled.span` + font: var(--font-mini-book); + color: var(--color-text-0); + + padding: 0 8rem; + text-align: center; +`; diff --git a/src/pages/token/rewards/RewardsPage.tsx b/src/pages/token/rewards/RewardsPage.tsx index 971c7d46f..023b1627e 100644 --- a/src/pages/token/rewards/RewardsPage.tsx +++ b/src/pages/token/rewards/RewardsPage.tsx @@ -30,6 +30,8 @@ const RewardsPage = () => { const { isTablet, isNotTablet } = useBreakpoints(); const navigate = useNavigate(); + const showChartPanel = testFlags.tradingRewardsRehaul; + return (
{isTablet && ( @@ -39,14 +41,17 @@ const RewardsPage = () => { /> )} - <$GridLayout showMigratePanel={import.meta.env.VITE_V3_TOKEN_ADDRESS && isNotTablet}> + <$GridLayout + showMigratePanel={import.meta.env.VITE_V3_TOKEN_ADDRESS && isNotTablet} + showChartPanel={showChartPanel} + > {import.meta.env.VITE_V3_TOKEN_ADDRESS && isNotTablet && <$MigratePanel />} {isTablet ? ( <$LaunchIncentivesPanel /> ) : ( <> - {testFlags.tradingRewardsRehaul && <$TradingRewardsChartPanel />} + {showChartPanel && <$TradingRewardsChartPanel />} <$LaunchIncentivesPanel /> {testFlags.enableStaking ? <$StakingPanel /> : <$DYDXBalancePanel />} @@ -54,7 +59,7 @@ const RewardsPage = () => { <$TradingRewardsColumn> - {isTablet && testFlags.tradingRewardsRehaul && } + {isTablet && showChartPanel && } {isTablet && } @@ -72,7 +77,7 @@ const RewardsPage = () => { export default RewardsPage; -const $GridLayout = styled.div<{ showMigratePanel?: boolean }>` +const $GridLayout = styled.div<{ showMigratePanel?: boolean; showChartPanel?: boolean }>` --gap: 1.5rem; display: grid; grid-template-columns: 2fr 1fr; @@ -83,9 +88,8 @@ const $GridLayout = styled.div<{ showMigratePanel?: boolean }>` gap: var(--gap); } - // xccx figure out migrate - ${({ showMigratePanel }) => - showMigratePanel + ${({ showMigratePanel, showChartPanel }) => + showMigratePanel && showChartPanel ? css` grid-template-areas: 'migrate migrate' @@ -94,12 +98,25 @@ const $GridLayout = styled.div<{ showMigratePanel?: boolean }>` 'balance balance' 'rewards other'; ` - : css` - grid-template-areas: - 'chart chart' - 'incentives balance' - 'rewards other'; - `} + : showMigratePanel + ? css` + grid-template-areas: + 'migrate migrate' + 'incentives balance' + 'rewards other'; + ` + : showChartPanel + ? css` + grid-template-areas: + 'chart chart' + 'incentives balance' + 'rewards other'; + ` + : css` + grid-template-areas: + 'incentives balance' + 'rewards other'; + `} @media ${breakpoints.notTablet} { padding: 1rem; @@ -114,6 +131,10 @@ const $GridLayout = styled.div<{ showMigratePanel?: boolean }>` grid-template-areas: 'incentives' 'rewards'; + + > :last-child { + margin-bottom: var(--gap); + } } `; diff --git a/src/views/charts/PnlChart.tsx b/src/views/charts/PnlChart.tsx index 10ca2bb62..889e56b05 100644 --- a/src/views/charts/PnlChart.tsx +++ b/src/views/charts/PnlChart.tsx @@ -1,4 +1,4 @@ -import { useEffect, useMemo, useState, type ReactNode } from 'react'; +import { useCallback, useEffect, useMemo, useState, type ReactNode } from 'react'; import { curveLinear } from '@visx/curve'; import type { TooltipContextType } from '@visx/xychart'; @@ -50,14 +50,6 @@ export type PnlDatum = { const PNL_TIME_RESOLUTION = 1 * timeUnits.hour; -const MS_FOR_PERIOD = { - [HistoricalPnlPeriod.Period1d.name]: 1 * timeUnits.day, - [HistoricalPnlPeriod.Period7d.name]: 7 * timeUnits.day, - [HistoricalPnlPeriod.Period30d.name]: 30 * timeUnits.day, - [HistoricalPnlPeriod.Period90d.name]: 90 * timeUnits.day, -}; - -const zoomDomainDefaultValues = new Set(Object.values(MS_FOR_PERIOD)); const getPeriodFromName = (periodName: string) => HISTORICAL_PNL_PERIODS[periodName as keyof typeof HISTORICAL_PNL_PERIODS]; @@ -108,22 +100,6 @@ export const PnlChart = ({ abacusStateManager.setHistoricalPnlPeriod(HistoricalPnlPeriod.Period90d); }, []); - const onSelectPeriod = (periodName: string) => setSelectedPeriod(getPeriodFromName(periodName)); - - // Unselect selected period in toggle if user zooms in/out - const onZoomSnap = useMemo( - () => - debounce(({ zoomDomain }: { zoomDomain?: number }) => { - if (zoomDomain) { - setIsZooming(!zoomDomainDefaultValues.has(zoomDomain)); - } - }, 200), - [] - ); - - // Snap back to default zoom domain according to selected period - const onToggleInteract = () => setIsZooming(false); - const lastPnlTick = pnlData?.[pnlData.length - 1]; const data = useMemo( @@ -156,24 +132,76 @@ export const PnlChart = ({ }) as PnlDatum ) : [], - [pnlData, equity?.current, now] + [pnlData, equity?.current, now, lastPnlTick, subaccountId] + ); + + const msForPeriod = useCallback( + (period: HistoricalPnlPeriods, clampMax: Boolean = true) => { + const earliestCreatedAt = data?.[0]?.createdAt; + const latestCreatedAt = data?.[data.length - 1]?.createdAt; + const maxPeriod = + earliestCreatedAt && latestCreatedAt + ? latestCreatedAt - earliestCreatedAt + : 90 * timeUnits.day; + switch (period) { + case HistoricalPnlPeriod.Period1d: + return clampMax ? Math.min(maxPeriod, 1 * timeUnits.day) : 1 * timeUnits.day; + case HistoricalPnlPeriod.Period7d: + return clampMax ? Math.min(maxPeriod, 7 * timeUnits.day) : 7 * timeUnits.day; + case HistoricalPnlPeriod.Period30d: + return clampMax ? Math.min(maxPeriod, 30 * timeUnits.day) : 30 * timeUnits.day; + case HistoricalPnlPeriod.Period90d: + default: + return clampMax ? Math.min(maxPeriod, 90 * timeUnits.day) : 90 * timeUnits.day; + } + }, + [data] ); + const onSelectPeriod = (periodName: string) => setSelectedPeriod(getPeriodFromName(periodName)); + + // Unselect selected period in toggle if user zooms in/out + const onZoomSnap = useMemo( + () => + debounce(({ zoomDomain }: { zoomDomain?: number }) => { + if (zoomDomain) { + const defaultPeriodIx = periodOptions.findIndex( + // To account for slight variance from zoom animation + (period) => Math.abs(msForPeriod(period) - zoomDomain) <= 1 + ); + + if (defaultPeriodIx < 0) { + setIsZooming(true); + } else { + setIsZooming(false); + setSelectedPeriod(periodOptions[defaultPeriodIx]); + } + } + }, 200), + [periodOptions, msForPeriod] + ); + + // Snap back to default zoom domain according to selected period + const onToggleInteract = () => setIsZooming(false); + // Include period option if oldest pnl is older than the previous option // e.g. oldest pnl is 31 days old -> show 90d option - const getPeriodOptions = (oldestPnlMs: number): HistoricalPnlPeriods[] => - Object.entries(MS_FOR_PERIOD).reduce( - (acc: HistoricalPnlPeriods[], [, ms], i, arr) => { - if (oldestPnlMs < now - ms) { - const nextPeriod = get(arr, [i + 1, 0]); - if (nextPeriod) { - acc.push(getPeriodFromName(nextPeriod)); + const getPeriodOptions = useCallback( + (oldestPnlMs: number): HistoricalPnlPeriods[] => + Object.entries(HISTORICAL_PNL_PERIODS).reduce( + (acc: HistoricalPnlPeriods[], [, period], i, arr) => { + if (oldestPnlMs < now - msForPeriod(period, false)) { + const nextPeriod = get(arr, [i + 1, 0]); + if (nextPeriod) { + acc.push(getPeriodFromName(nextPeriod)); + } } - } - return acc; - }, - [HistoricalPnlPeriod.Period1d] - ); + return acc; + }, + [HistoricalPnlPeriod.Period1d] + ), + [msForPeriod, now] + ); const oldestPnlCreatedAt = pnlData?.[0]?.createdAtMilliseconds; @@ -183,56 +211,78 @@ export const PnlChart = ({ setPeriodOptions(options); // default to show 7d period if there's enough data - if (options[options.length - 1] === HistoricalPnlPeriod.Period7d) + if (options.includes(HistoricalPnlPeriod.Period7d)) { setSelectedPeriod(HistoricalPnlPeriod.Period7d); + } } - }, [oldestPnlCreatedAt]); + }, [oldestPnlCreatedAt, getPeriodOptions]); + + const chartStyles = useMemo( + () => ({ + background: + appTheme === AppTheme.Light ? LIGHT_CHART_BACKGROUND_URL : DARK_CHART_BACKGROUND_URL, + margin: { + left: -0.5, // left: isMobile ? -0.5 : 70, + right: -0.5, + top: 0, + bottom: 32, + }, + padding: { + left: 0.01, + right: 0.01, + top: isTablet ? 0.5 : 0.15, + bottom: 0.1, + }, + }), + [appTheme, isTablet] + ); - const chartBackground = - appTheme === AppTheme.Light ? LIGHT_CHART_BACKGROUND_URL : DARK_CHART_BACKGROUND_URL; + const xAccessorFunc = useCallback((datum: PnlDatum) => datum?.createdAt, []); + const yAccessorFunc = useCallback((datum: PnlDatum) => datum?.equity, []); + + const series = useMemo( + () => [ + { + dataKey: 'pnl', + xAccessor: xAccessorFunc, + yAccessor: yAccessorFunc, + colorAccessor: () => 'var(--pnl-line-color)', + getCurve: () => curveLinear, + }, + ], + [xAccessorFunc, yAccessorFunc] + ); + + const tickFormatY = useCallback( + (value: number) => + new Intl.NumberFormat(selectedLocale, { + style: 'currency', + currency: 'USD', + notation: 'compact', + maximumSignificantDigits: 3, + }) + .format(Math.abs(value)) + .toLowerCase(), + [selectedLocale] + ); + + const renderTooltip = useCallback(() =>
, []); return ( - <$Container className={className} chartBackground={chartBackground}> + <$Container className={className} chartBackground={chartStyles.background}> datum?.createdAt, - yAccessor: (datum) => datum?.equity, - colorAccessor: () => 'var(--pnl-line-color)', - getCurve: () => curveLinear, - }, - ]} - tickFormatY={(value) => - new Intl.NumberFormat(selectedLocale, { - style: 'currency', - currency: 'USD', - notation: 'compact', - maximumSignificantDigits: 3, - }) - .format(Math.abs(value)) - .toLowerCase() - } - renderTooltip={() =>
} + margin={chartStyles.margin} + padding={chartStyles.padding} + series={series} + tickFormatY={tickFormatY} + renderTooltip={renderTooltip} onTooltipContext={onTooltipContext} onVisibleDataChange={onVisibleDataChange} onZoom={onZoomSnap} slotEmpty={slotEmpty} - defaultZoomDomain={isZooming ? undefined : MS_FOR_PERIOD[selectedPeriod.name]} + defaultZoomDomain={isZooming ? undefined : msForPeriod(selectedPeriod, false)} minZoomDomain={PNL_TIME_RESOLUTION * 2} numGridLines={0} tickSpacingX={210} @@ -242,7 +292,7 @@ export const PnlChart = ({ ({ value: period.name, - label: formatRelativeTime(MS_FOR_PERIOD[period.name], { + label: formatRelativeTime(msForPeriod(period, false), { locale: selectedLocale, relativeToTimestamp: 0, largestUnit: 'day', diff --git a/src/views/charts/TradingRewardsChart.tsx b/src/views/charts/TradingRewardsChart.tsx index f1b7b0839..961385878 100644 --- a/src/views/charts/TradingRewardsChart.tsx +++ b/src/views/charts/TradingRewardsChart.tsx @@ -1,11 +1,16 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { kollections } from '@dydxprotocol/v4-abacus'; import { curveLinear } from '@visx/curve'; import { TooltipContextType } from '@visx/xychart'; import { debounce } from 'lodash'; import styled from 'styled-components'; -import { HistoricalTradingRewardsPeriod } from '@/constants/abacus'; +import { + HistoricalTradingReward, + HistoricalTradingRewardsPeriod, + Nullable, +} from '@/constants/abacus'; import { TradingRewardsPeriod, tradingRewardsPeriods, @@ -71,84 +76,134 @@ export const TradingRewardsChart = ({ const rewardsHistoryStartDate = useEnvConfig('rewardsHistoryStartDateMs'); const now = useNow({ intervalMs: timeUnits.minute }); + const [periodOptions, setPeriodOptions] = useState([ + TradingRewardsPeriod.Period1d, + ]); const [tooltipContext, setTooltipContext] = useState>(); const [isZooming, setIsZooming] = useState(false); const [selectedPeriod, setSelectedPeriod] = useState( TradingRewardsPeriod.PeriodAllTime ); + const [defaultZoomDomain, setDefaultZoomDomain] = useState(undefined); const canViewAccount = useAppSelector(calculateCanViewAccount); const totalTradingRewards = useAppSelector(getTotalTradingRewards); - const periodTradingRewards = useParameterizedSelector( - getHistoricalTradingRewardsForPeriod, - SELECTED_PERIOD.name - ); + const periodTradingRewards: Nullable> = + useParameterizedSelector(getHistoricalTradingRewardsForPeriod, SELECTED_PERIOD.name); useEffect(() => { // Initialize daily data for rewards chart abacusStateManager.setHistoricalTradingRewardPeriod(HistoricalTradingRewardsPeriod.DAILY); }, [canViewAccount]); + const rewardsData = useMemo( + () => + periodTradingRewards && canViewAccount + ? periodTradingRewards + .toArray() + .reverse() + .map( + (datum): TradingRewardsDatum => ({ + date: new Date(datum.endedAtInMilliseconds).valueOf(), + cumulativeAmount: datum.cumulativeAmount, + }) + ) + : [], + [periodTradingRewards, canViewAccount] + ); + + const oldestDataPointDate = rewardsData?.[0]?.date; + const newestDataPointDate = rewardsData?.[rewardsData.length - 1]?.date; + const msForPeriod = useCallback( - (period: TradingRewardsPeriod) => { + (period: TradingRewardsPeriod, clampMax: Boolean = true) => { + const earliestDatum = oldestDataPointDate ?? Number(rewardsHistoryStartDate); + const latestDatum = newestDataPointDate ?? now; + const maxPeriod = latestDatum - earliestDatum; + switch (period) { case TradingRewardsPeriod.Period1d: - return 1 * timeUnits.day; + return clampMax ? Math.min(maxPeriod, 1 * timeUnits.day) : 1 * timeUnits.day; case TradingRewardsPeriod.Period7d: - return 7 * timeUnits.day; + return clampMax ? Math.min(maxPeriod, 7 * timeUnits.day) : 7 * timeUnits.day; case TradingRewardsPeriod.Period30d: - return 30 * timeUnits.day; + return clampMax ? Math.min(maxPeriod, 30 * timeUnits.day) : 30 * timeUnits.day; case TradingRewardsPeriod.Period90d: - return 90 * timeUnits.day; + return clampMax ? Math.min(maxPeriod, 90 * timeUnits.day) : 90 * timeUnits.day; case TradingRewardsPeriod.PeriodAllTime: default: - return (now - Number(rewardsHistoryStartDate) + 1) * timeUnits.day; + return maxPeriod; } }, - [now, rewardsHistoryStartDate] + [now, rewardsHistoryStartDate, newestDataPointDate, oldestDataPointDate] ); - // Unselect selected period in toggle if user zooms in/out + // Include period option only if oldest date is older it + // e.g. oldest date is 31 days old -> show 30d option, but not 90d + const getPeriodOptions = useCallback( + (oldestMs: number): TradingRewardsPeriod[] => + tradingRewardsPeriods.reduce((acc: TradingRewardsPeriod[], period) => { + if (oldestMs <= (newestDataPointDate ?? now) - msForPeriod(period, false)) { + acc.push(period); + } + return acc; + }, []), + [msForPeriod, newestDataPointDate, now] + ); + + useEffect(() => { + if (oldestDataPointDate) { + const options = getPeriodOptions(oldestDataPointDate); + setPeriodOptions(options); + } + }, [oldestDataPointDate, getPeriodOptions]); + + // Update selected period in toggle if user zooms in/out const onZoomSnap = useMemo( () => debounce(({ zoomDomain }: { zoomDomain?: number }) => { if (zoomDomain) { - setIsZooming(!tradingRewardsPeriods.map(msForPeriod).includes(zoomDomain)); + const predefinedPeriodIx = periodOptions.findIndex( + // To account for slight variance from zoom animation + (period) => Math.abs(msForPeriod(period) - zoomDomain) <= 1 + ); + if (predefinedPeriodIx < 0) { + // Unselect period + setIsZooming(true); + } else { + // Update period to new selected period + setIsZooming(false); + setSelectedPeriod(periodOptions[predefinedPeriodIx]); + } } }, 200), - [msForPeriod] + [periodOptions, msForPeriod] ); + useEffect(() => { + if (isZooming) { + setDefaultZoomDomain(undefined); + } else { + setDefaultZoomDomain(msForPeriod(selectedPeriod)); + } + }, [isZooming, msForPeriod, selectedPeriod]); + const onToggleInteract = () => setIsZooming(false); - const rewardsData = useMemo( - () => - periodTradingRewards && canViewAccount - ? periodTradingRewards - .toArray() - .reverse() - .map( - (datum) => - ({ - date: new Date(datum.endedAtInMilliseconds).valueOf(), - cumulativeAmount: datum.cumulativeAmount, - }) as TradingRewardsDatum - ) - : [], - [periodTradingRewards, canViewAccount] - ); + const xAccessorFunc = useCallback((datum: TradingRewardsDatum) => datum?.date, []); + const yAccessorFunc = useCallback((datum: TradingRewardsDatum) => datum?.cumulativeAmount, []); const series = useMemo( () => [ { dataKey: 'trading-rewards', - xAccessor: (datum: TradingRewardsDatum) => datum?.date, - yAccessor: (datum: TradingRewardsDatum) => datum?.cumulativeAmount, + xAccessor: xAccessorFunc, + yAccessor: yAccessorFunc, colorAccessor: () => 'var(--trading-rewards-line-color)', getCurve: () => curveLinear, }, ], - [] + [xAccessorFunc, yAccessorFunc] ); const language = navigator.language || 'en-US'; @@ -166,26 +221,24 @@ export const TradingRewardsChart = ({ ); const renderTooltip = useCallback(() =>
, []); - const defaultZoomDomain = isZooming ? undefined : msForPeriod(selectedPeriod); const toggleGroupItems = useMemo(() => { - return tradingRewardsPeriods.map((period: TradingRewardsPeriod) => ({ + return periodOptions.map((period: TradingRewardsPeriod) => ({ value: period, label: period === TradingRewardsPeriod.PeriodAllTime ? stringGetter({ key: STRING_KEYS.ALL }) - : formatRelativeTime(msForPeriod(period), { + : formatRelativeTime(msForPeriod(period, false), { locale: selectedLocale, relativeToTimestamp: 0, largestUnit: 'day', }), })); - }, [stringGetter, msForPeriod, selectedLocale]); + }, [stringGetter, msForPeriod, selectedLocale, periodOptions]); - const setTradingRewardsPeriod = useCallback( - (value: string) => setSelectedPeriod(value as TradingRewardsPeriod), - [] - ); + const setTradingRewardsPeriod = useCallback((value: string) => { + setSelectedPeriod(value as TradingRewardsPeriod); + }, []); return ( <> diff --git a/src/views/tables/TradingRewardHistoryTable.tsx b/src/views/tables/TradingRewardHistoryTable.tsx index 059abbf43..e729398ec 100644 --- a/src/views/tables/TradingRewardHistoryTable.tsx +++ b/src/views/tables/TradingRewardHistoryTable.tsx @@ -1,19 +1,26 @@ -import { useMemo } from 'react'; +import { useCallback, useMemo } from 'react'; +import { kollections } from '@dydxprotocol/v4-abacus'; import styled from 'styled-components'; -import { HistoricalTradingReward, HistoricalTradingRewardsPeriods } from '@/constants/abacus'; +import { + HistoricalTradingReward, + HistoricalTradingRewardsPeriods, + Nullable, +} from '@/constants/abacus'; import { STRING_KEYS, type StringGetterFunction } from '@/constants/localization'; +import { useBreakpoints } from '@/hooks/useBreakpoints'; import { useParameterizedSelector } from '@/hooks/useParameterizedSelector'; import { useStringGetter } from '@/hooks/useStringGetter'; import { useTokenConfigs } from '@/hooks/useTokenConfigs'; import { layoutMixins } from '@/styles/layoutMixins'; +import { tradeViewMixins } from '@/styles/tradeViewMixins'; import { AssetIcon } from '@/components/AssetIcon'; import { Icon, IconName } from '@/components/Icon'; -import { Output, OutputType } from '@/components/Output'; +import { Output, OutputType, ShowSign } from '@/components/Output'; import { Table, type ColumnDef } from '@/components/Table'; import { TableCell } from '@/components/Table/TableCell'; @@ -74,10 +81,11 @@ const getTradingRewardHistoryTableColumnDef = ({ getCellValue: (row) => row.amount, label: stringGetter({ key: STRING_KEYS.EARNED }), renderCell: ({ amount }) => ( - } + showSign={ShowSign.Both} + slotRight={} /> ), }, @@ -91,42 +99,43 @@ type ElementProps = { }; type StyleProps = { - withOuterBorder?: boolean; - withInnerBorders?: boolean; + className?: string; }; export const TradingRewardHistoryTable = ({ period, columnKeys = Object.values(TradingRewardHistoryTableColumnKey), - withOuterBorder, - withInnerBorders = true, + className, }: ElementProps & StyleProps) => { const stringGetter = useStringGetter(); const canViewAccount = useAppSelector(calculateCanViewAccount); + const { isNotTablet } = useBreakpoints(); const { chainTokenLabel } = useTokenConfigs(); - const periodTradingRewards = useParameterizedSelector( - getHistoricalTradingRewardsForPeriod, - period.name - ); + const periodTradingRewards: Nullable> = + useParameterizedSelector(getHistoricalTradingRewardsForPeriod, period.name); + + const rewardsData = useMemo(() => { + return periodTradingRewards && canViewAccount ? periodTradingRewards.toArray() : []; + }, [periodTradingRewards, canViewAccount]); - const rewardsData = useMemo( - () => (periodTradingRewards && canViewAccount ? periodTradingRewards.toArray() : []), - [periodTradingRewards, canViewAccount] + const columns = columnKeys.map((key: TradingRewardHistoryTableColumnKey) => + getTradingRewardHistoryTableColumnDef({ + key, + chainTokenLabel, + stringGetter, + }) ); + const getRowKey = useCallback((row: any) => row.startedAtInMilliseconds, []); + return ( <$Table - label={stringGetter({ key: STRING_KEYS.REWARD_HISTORY })} + className={className} + label={stringGetter({ key: STRING_KEYS.TRADING_REWARD_HISTORY })} data={rewardsData} - getRowKey={(row: any) => row.startedAtInMilliseconds} - columns={columnKeys.map((key: TradingRewardHistoryTableColumnKey) => - getTradingRewardHistoryTableColumnDef({ - key, - chainTokenLabel, - stringGetter, - }) - )} + getRowKey={getRowKey} + columns={columns} slotEmpty={ <$Column> <$EmptyIcon iconName={IconName.OrderPending} /> @@ -134,8 +143,8 @@ export const TradingRewardHistoryTable = ({ } selectionBehavior="replace" - withOuterBorder={withOuterBorder} - withInnerBorders={withInnerBorders} + withOuterBorder={isNotTablet || rewardsData.length === 0} + withInnerBorders initialPageSize={15} withScrollSnapColumns withScrollSnapRows @@ -144,12 +153,10 @@ export const TradingRewardHistoryTable = ({ }; const $Table = styled(Table)` - --tableCell-padding: 0.5rem 0; - --tableStickyRow-backgroundColor: var(--color-layer-3); - --tableRow-backgroundColor: var(--color-layer-3); + ${tradeViewMixins.horizontalTable} tbody { - font: var(--font-medium-book); + font: var(--font-small-book); } ` as typeof Table; @@ -171,10 +178,6 @@ const $TimePeriod = styled.div` } `; -const $AssetIcon = styled(AssetIcon)` - margin-left: 0.5ch; -`; - const $Column = styled.div` display: flex; flex-direction: column; @@ -185,3 +188,8 @@ const $Column = styled.div` const $EmptyIcon = styled(Icon)` font-size: 3em; `; + +const $PositiveOutput = styled(Output)` + --output-sign-color: var(--color-positive); + gap: 0.5ch; +`;