From 936525510b0995dec3e1cd89544153cfb88d43d3 Mon Sep 17 00:00:00 2001 From: KacperKoza343 Date: Wed, 14 Aug 2024 17:40:54 +0200 Subject: [PATCH 1/4] feat(app/graph-page): integrate graph page with api --- packages/apps/dashboard/ui-2024/package.json | 1 + .../src/components/Charts/AreaChart.tsx | 194 +++++------------- .../components/DataEntry/ToggleButtons.tsx | 32 ++- .../ui-2024/src/pages/Graph/Graph.tsx | 16 +- .../ui-2024/src/services/api-paths.ts | 6 + .../api/use-graph-page-chart-data.tsx | 116 +++++++++++ .../ui-2024/src/services/validate-response.ts | 13 +- .../hooks/use-graph-page-chart-params.tsx | 95 +++++++++ 8 files changed, 303 insertions(+), 170 deletions(-) create mode 100644 packages/apps/dashboard/ui-2024/src/services/api/use-graph-page-chart-data.tsx create mode 100644 packages/apps/dashboard/ui-2024/src/utils/hooks/use-graph-page-chart-params.tsx diff --git a/packages/apps/dashboard/ui-2024/package.json b/packages/apps/dashboard/ui-2024/package.json index 0de623b2e0..64cfa161c8 100644 --- a/packages/apps/dashboard/ui-2024/package.json +++ b/packages/apps/dashboard/ui-2024/package.json @@ -34,6 +34,7 @@ "simplebar-react": "^3.2.5", "styled-components": "^6.1.11", "swiper": "^11.1.3", + "use-debounce": "^10.0.2", "zod": "^3.23.8", "zustand": "^4.5.4" }, diff --git a/packages/apps/dashboard/ui-2024/src/components/Charts/AreaChart.tsx b/packages/apps/dashboard/ui-2024/src/components/Charts/AreaChart.tsx index 41e9b85f09..4a8caddcb3 100644 --- a/packages/apps/dashboard/ui-2024/src/components/Charts/AreaChart.tsx +++ b/packages/apps/dashboard/ui-2024/src/components/Charts/AreaChart.tsx @@ -8,7 +8,7 @@ import { ResponsiveContainer, } from 'recharts'; import CustomChartTooltip from './CustomChartTooltip'; -import { useEffect, useRef, useState } from 'react'; +import { useEffect, useMemo, useRef, useState } from 'react'; import Card from '@mui/material/Card'; import { Typography } from '@mui/material'; import Stack from '@mui/material/Stack'; @@ -16,105 +16,14 @@ import { colorPalette } from '@assets/styles/color-palette'; import CustomXAxisTick from '@components/Charts/CustomXAxisTick'; import DatePicker from '@components/DataEntry/DatePicker'; import ToggleButtons from '@components/DataEntry/ToggleButtons'; -import dayjs, { Dayjs } from 'dayjs'; +import { Dayjs } from 'dayjs'; import ToggleCharts from '@components/Charts/ToggleCharts'; import { formatNumber } from '@helpers/formatNumber'; - -const HARDCODED_CHART_DATA = [ - { - name: '2024-01-01', - transferAmount: 100, - transactionsCount: 2000, - uniqueReceivers: 3000, - uniqueSenders: 200, - }, - { - name: '2024-01-02', - transferAmount: 150, - transactionsCount: 2200, - uniqueReceivers: 3200, - uniqueSenders: 250, - }, - { - name: '2024-01-03', - transferAmount: 200, - transactionsCount: 2500, - uniqueReceivers: 3500, - uniqueSenders: 300, - }, - { - name: '2024-01-04', - transferAmount: 120, - transactionsCount: 2100, - uniqueReceivers: 3100, - uniqueSenders: 220, - }, - { - name: '2024-01-05', - transferAmount: 180, - transactionsCount: 2300, - uniqueReceivers: 3400, - uniqueSenders: 270, - }, - { - name: '2024-01-06', - transferAmount: 130, - transactionsCount: 2400, - uniqueReceivers: 3300, - uniqueSenders: 230, - }, - { - name: '2024-01-07', - transferAmount: 170, - transactionsCount: 2600, - uniqueReceivers: 3600, - uniqueSenders: 280, - }, - { - name: '2024-01-08', - transferAmount: 140, - transactionsCount: 2700, - uniqueReceivers: 3700, - uniqueSenders: 2400, - }, - { - name: '2024-01-09', - transferAmount: 160, - transactionsCount: 2800, - uniqueReceivers: 3800, - uniqueSenders: 2900, - }, - { - name: '2024-01-10', - transferAmount: 190, - transactionsCount: 600, - uniqueReceivers: 1000, - uniqueSenders: 5000, - }, -]; - -const TIME_PERIOD_OPTIONS = [ - { - value: '1W', - name: '1W', - }, - { - value: '1M', - name: '1M', - }, - { - value: '6M', - name: '6M', - }, - { - value: '1Y', - name: '1Y', - }, - { - value: 'All', - name: 'All', - }, -]; +import { + GraphPageChartData, + useGraphPageChartData, +} from '@services/api/use-graph-page-chart-data'; +import { useGraphPageChartParams } from '@utils/hooks/use-graph-page-chart-params'; const CHECKED_CHARTS_DEFAULT_STATE = { transferAmount: true, @@ -129,11 +38,15 @@ const HOVERED_CHARTS_DEFAULT_STATE = { uniqueSenders: false, }; -export const AreaChart = () => { - const [chartData] = useState(HARDCODED_CHART_DATA); - const [selectedTimePeriod, selectTimePeriod] = useState('1W'); - const [fromDate, setFromDate] = useState(dayjs(new Date())); - const [toDate, setToDate] = useState(dayjs(new Date())); +export const AreaChart = ({ chartData }: { chartData: GraphPageChartData }) => { + const { + setFromDate, + setToDate, + clearTimePeriod, + dateRangeParams: { from, to }, + } = useGraphPageChartParams(); + + const lastIndex = chartData.length - 1; const [checkedCharts, setCheckedCharts] = useState( CHECKED_CHARTS_DEFAULT_STATE ); @@ -157,15 +70,6 @@ export const AreaChart = () => { if (value) setToDate(value); }; - const handleTimePeriod = ( - _event: React.MouseEvent, - value: string | null - ) => { - if (value !== null) { - selectTimePeriod(value); - } - }; - const onChartHover = (name: string) => { setCurrentHoveredChart((prevState) => ({ ...prevState, @@ -181,17 +85,15 @@ export const AreaChart = () => { const currentRef = chartRef.current; if (currentRef) { const handleScrollChangeDate = (event: WheelEvent) => { + event.preventDefault(); + clearTimePeriod(); if (event.deltaY < 0) { - setFromDate((prevState) => { - if (prevState.isAfter(toDate) || prevState.isSame(toDate)) { - return prevState; - } - return prevState.add(1, 'day'); - }); + if (from.add(1, 'day').isAfter(to) || from.add(1, 'day').isSame(to)) { + return; + } + setFromDate(from.add(1, 'day')); } else if (event.deltaY > 0) { - setFromDate((prevState) => { - return prevState.subtract(1, 'day'); - }); + setFromDate(from.subtract(1, 'day')); } }; @@ -201,7 +103,7 @@ export const AreaChart = () => { currentRef.removeEventListener('wheel', handleScrollChangeDate); }; } - }, [toDate]); + }, [clearTimePeriod, from, setFromDate, to]); return ( { direction={{ xs: 'column', md: 'row' }} gap={{ xs: 6, md: 8 }} > - + - + - - + { /> { /> { stopOpacity={currentHoveredChart.uniqueReceivers ? 1 : 0} /> - + { {checkedCharts.transferAmount && ( { {checkedCharts.transactionsCount && ( { {checkedCharts.uniqueReceivers && ( { {checkedCharts.uniqueSenders && ( { { title: 'Transfer Amount', isAreaChart: true, - name: 'transferAmount', - amount: chartData[chartData.length - 1].transferAmount, + name: 'totalTransactionAmount', + amount: chartData[lastIndex]?.totalTransactionAmount, color: colorPalette.primary.main, }, { title: 'Transactions Count', - name: 'transactionsCount', - amount: chartData[chartData.length - 1].transactionsCount, + name: 'totalTransactionCount', + amount: chartData[lastIndex]?.totalTransactionCount, color: colorPalette.secondary.main, }, { title: 'Unique Receivers', - name: 'uniqueReceivers', - amount: chartData[chartData.length - 1].uniqueReceivers, + name: 'dailyUniqueReceivers', + amount: chartData[lastIndex]?.dailyUniqueReceivers, color: colorPalette.error.main, }, { title: 'Unique Senders', - name: 'uniqueSenders', - amount: chartData[chartData.length - 1].uniqueSenders, + name: 'dailyUniqueSenders', + amount: chartData[lastIndex]?.dailyUniqueSenders, color: colorPalette.success.main, }, ]} diff --git a/packages/apps/dashboard/ui-2024/src/components/DataEntry/ToggleButtons.tsx b/packages/apps/dashboard/ui-2024/src/components/DataEntry/ToggleButtons.tsx index 5360ef95ae..e56aa90f03 100644 --- a/packages/apps/dashboard/ui-2024/src/components/DataEntry/ToggleButtons.tsx +++ b/packages/apps/dashboard/ui-2024/src/components/DataEntry/ToggleButtons.tsx @@ -3,6 +3,10 @@ import Typography from '@mui/material/Typography'; import { styled } from '@mui/material'; import ToggleButtonGroup from '@mui/material/ToggleButtonGroup'; import { colorPalette } from '@assets/styles/color-palette'; +import { + TIME_PERIOD_OPTIONS, + useGraphPageChartParams, +} from '@utils/hooks/use-graph-page-chart-params'; export const StyledToggleButtonGroup = styled(ToggleButtonGroup)({ '.MuiToggleButtonGroup-grouped': { @@ -13,29 +17,21 @@ export const StyledToggleButtonGroup = styled(ToggleButtonGroup)({ }, }); -interface ToggleButtonsProps { - buttonOptions: { name: string; value: string }[]; - selectedValue: string; - onValueChange: ( - _event: React.MouseEvent, - value: string | null - ) => void; -} - -const ToggleButtons = ({ - selectedValue, - onValueChange, - buttonOptions, -}: ToggleButtonsProps) => { +const ToggleButtons = () => { + const { setTimePeriod, selectedTimePeriod, dateRangeParams } = + useGraphPageChartParams(); return ( - {buttonOptions.map((elem) => ( + {TIME_PERIOD_OPTIONS.map((elem) => ( { + setTimePeriod(elem); + }} + selected={elem.value.isSame(dateRangeParams.from)} key={elem.name} sx={{ '.MuiTypography-root': { @@ -50,7 +46,7 @@ const ToggleButtons = ({ backgroundColor: colorPalette.primary.main, }, }} - value={elem.value} + value={elem.name} > {elem.name} diff --git a/packages/apps/dashboard/ui-2024/src/pages/Graph/Graph.tsx b/packages/apps/dashboard/ui-2024/src/pages/Graph/Graph.tsx index 39b889f392..c484050a41 100644 --- a/packages/apps/dashboard/ui-2024/src/pages/Graph/Graph.tsx +++ b/packages/apps/dashboard/ui-2024/src/pages/Graph/Graph.tsx @@ -2,20 +2,30 @@ import { LineChart, AreaChart } from '@components/Charts'; import Tabs from '@mui/material/Tabs'; import TabPanel from '@mui/lab/TabPanel'; import Tab from '@mui/material/Tab'; -import { useState } from 'react'; +import { useMemo, useState } from 'react'; import TabContext from '@mui/lab/TabContext'; import Typography from '@mui/material/Typography'; import PageWrapper from '@components/PageWrapper'; import Breadcrumbs from '@components/Breadcrumbs'; +import { useGraphPageChartData } from '@services/api/use-graph-page-chart-data'; type graphType = 'bucketed' | 'cumulative'; const Graph = () => { const [graphType, setGraphType] = useState('bucketed'); - + const { data, isLoading, isError } = useGraphPageChartData(); + const memoizedData = useMemo(() => data, [data]); const handleGraphTypeChange = (_: unknown, newValue: graphType) => { setGraphType(newValue); }; + if (isLoading) { + return '...Loading'; + } + + if (isError) { + return 'Error'; + } + return ( @@ -48,7 +58,7 @@ const Graph = () => { }} value="bucketed" > - + { + const valueAsNumber = Number(value); + if (Number.isNaN(valueAsNumber)) { + ctx.addIssue({ + path: ['totalTransactionAmount'], + code: z.ZodIssueCode.custom, + }); + } + + return valueAsNumber / 10 ** 18; + }), + totalTransactionCount: z.number(), + dailyUniqueSenders: z.number(), + dailyUniqueReceivers: z.number(), + date: z.string(), + }) + ), +}); + +export type HMTDailyStatsResponse = z.output< + typeof hmtDailyStatSchemaResponseSchema +>; +export type HMTDailyStat = HMTDailyStatsResponse['results'][number]; + +const hcaptchaDailyStatsResponseSchema = z.object({ + from: z.string().optional(), + to: z.string().optional(), + results: z.array( + z.object({ + served: z.number(), + solved: z.number(), + date: z.string(), + }) + ), +}); + +export type HcaptchaDailyStatsResponse = z.infer< + typeof hcaptchaDailyStatsResponseSchema +>; +export type HcaptchaDailyStat = HcaptchaDailyStatsResponse['results'][number]; + +export type GraphPageChartData = (HMTDailyStat & + Omit)[]; + +const mergeResponses = ( + hcaptchaStatsResults: HcaptchaDailyStat[], + hmtStatsResults: HMTDailyStat[] +): GraphPageChartData => { + const result: GraphPageChartData = []; + // console.log({ hcaptchaStatsResults: , hmtStatsResults }); + + for (let i = 0; i < hcaptchaStatsResults.length; i++) { + result.push({ ...hcaptchaStatsResults[i], ...hmtStatsResults[i] }); + console.log(i); + } + return result; +}; + +const DEBOUNCE_MS = 300; + +export function useGraphPageChartData() { + const { dateRangeParams } = useGraphPageChartParams(); + const queryParams = { + from: dateRangeParams.from.format('YYYY-MM-DD'), + to: dateRangeParams.to.format('YYYY-MM-DD'), + }; + const [debouncedQueryParams] = useDebounce(queryParams, DEBOUNCE_MS); + + return useQuery({ + queryFn: async () => { + const { data: hmtDailyStats } = await httpService.get( + apiPaths.hmtDailyStats.path, + { + params: debouncedQueryParams, + } + ); + const { data: hcaptchDailyStats } = await httpService.get( + apiPaths.hcaptchaStatsDaily.path, + { + params: debouncedQueryParams, + } + ); + + const validHmtDailyStats = validateResponse( + hmtDailyStats, + hmtDailyStatSchemaResponseSchema + ); + + const validHcaptchaGeneralStats = validateResponse( + hcaptchDailyStats, + hcaptchaDailyStatsResponseSchema + ); + + return mergeResponses( + validHcaptchaGeneralStats.results, + validHmtDailyStats.results + ); + }, + staleTime: DEBOUNCE_MS, + queryKey: ['useGraphPageChartData', debouncedQueryParams], + placeholderData: keepPreviousData, + }); +} diff --git a/packages/apps/dashboard/ui-2024/src/services/validate-response.ts b/packages/apps/dashboard/ui-2024/src/services/validate-response.ts index 271468dbdf..9cc6c04d08 100644 --- a/packages/apps/dashboard/ui-2024/src/services/validate-response.ts +++ b/packages/apps/dashboard/ui-2024/src/services/validate-response.ts @@ -1,15 +1,20 @@ -import { type z } from 'zod'; +import { ZodError, type z } from 'zod'; -export const validateResponse = ( +export const validateResponse = ( object: unknown, - zodObject: z.ZodSchema -): T => { + zodObject: T +): z.infer => { try { const data = zodObject.parse(object); return data; } catch (error) { console.error('Unexpected response'); + if (error instanceof ZodError) { + error.issues.forEach((issue) => { + console.log(issue); + }); + } console.error(error); throw error; } diff --git a/packages/apps/dashboard/ui-2024/src/utils/hooks/use-graph-page-chart-params.tsx b/packages/apps/dashboard/ui-2024/src/utils/hooks/use-graph-page-chart-params.tsx new file mode 100644 index 0000000000..5b757d9c12 --- /dev/null +++ b/packages/apps/dashboard/ui-2024/src/utils/hooks/use-graph-page-chart-params.tsx @@ -0,0 +1,95 @@ +import dayjs, { Dayjs } from 'dayjs'; +import { create } from 'zustand'; + +export type GraphPageChartPeriodName = '1W' | '1M' | '6M' | '1Y' | 'ALL'; + +export type TimePeriod = { + value: Dayjs; + name: GraphPageChartPeriodName; +}; + +const oneWeekAgo = dayjs().subtract(1, 'week'); +const oneMonthAgo = dayjs().subtract(1, 'month'); +const sixMonthsAgo = dayjs().subtract(6, 'months'); +const oneYearAgo = dayjs().subtract(1, 'year'); +const allTime = dayjs().subtract(10, 'years'); + +export const TIME_PERIOD_OPTIONS: TimePeriod[] = [ + { + value: oneWeekAgo, + name: '1W', + }, + { + value: oneMonthAgo, + name: '1M', + }, + { + value: sixMonthsAgo, + name: '6M', + }, + { + value: oneYearAgo, + name: '1Y', + }, + { + value: allTime, + name: 'ALL', + }, +]; + +export interface GraphPageChartParams { + dateRangeParams: { + from: Dayjs; + to: Dayjs; + }; + selectedTimePeriod: GraphPageChartPeriodName | null; + setTimePeriod: (timePeriod: TimePeriod) => void; + clearTimePeriod: () => void; + setFromDate: (fromDate: Dayjs | null) => void; + setToDate: (toDate: Dayjs | null) => void; +} + +export const useGraphPageChartParams = create((set) => ({ + dateRangeParams: { + from: oneWeekAgo, + to: dayjs(), + }, + selectedTimePeriod: '1W', + setFromDate: (fromDate: Dayjs | null) => { + if (!fromDate) { + return null; + } + set((state) => ({ + ...state, + dateRangeParams: { + ...state.dateRangeParams, + from: fromDate, + }, + })); + }, + setToDate: (toDate: Dayjs | null) => { + if (!toDate) { + return null; + } + set((state) => ({ + ...state, + dateRangeParams: { + ...state.dateRangeParams, + to: toDate, + }, + })); + }, + setTimePeriod: (timePeriod: TimePeriod) => { + set((state) => ({ + ...state, + selectedTimePeriod: timePeriod.name, + dateRangeParams: { + ...state.dateRangeParams, + from: timePeriod.value, + }, + })); + }, + clearTimePeriod: () => { + set((state) => ({ ...state, selectedTimePeriod: null })); + }, +})); From 03372bc8e225284582075c20061305f22394def2 Mon Sep 17 00:00:00 2001 From: KacperKoza343 Date: Sat, 17 Aug 2024 18:59:51 +0200 Subject: [PATCH 2/4] feat(app/graph-page): add missing chart --- .../src/assets/styles/color-palette.ts | 1 + .../src/components/Charts/AreaChart.tsx | 170 +++++++++++++----- .../components/Charts/CustomChartTooltip.tsx | 70 +++++--- .../src/components/Charts/ToggleCharts.tsx | 25 +-- .../src/components/DataEntry/DatePicker.tsx | 4 +- .../components/DataEntry/ToggleButtons.tsx | 21 ++- .../ui-2024/src/pages/Graph/Graph.tsx | 16 +- .../api/use-graph-page-chart-data.tsx | 83 +++++++-- .../hooks/use-graph-page-chart-params.tsx | 54 ++++-- 9 files changed, 315 insertions(+), 129 deletions(-) diff --git a/packages/apps/dashboard/ui-2024/src/assets/styles/color-palette.ts b/packages/apps/dashboard/ui-2024/src/assets/styles/color-palette.ts index 96a47d26c9..f4f0ec8500 100644 --- a/packages/apps/dashboard/ui-2024/src/assets/styles/color-palette.ts +++ b/packages/apps/dashboard/ui-2024/src/assets/styles/color-palette.ts @@ -43,6 +43,7 @@ export const colorPalette = { ocean: { main: '#304FFE', light: '#8C9EFF', + dark: '#03A9F4', }, orange: { main: '#ED6C02', diff --git a/packages/apps/dashboard/ui-2024/src/components/Charts/AreaChart.tsx b/packages/apps/dashboard/ui-2024/src/components/Charts/AreaChart.tsx index 4a8caddcb3..c361f56254 100644 --- a/packages/apps/dashboard/ui-2024/src/components/Charts/AreaChart.tsx +++ b/packages/apps/dashboard/ui-2024/src/components/Charts/AreaChart.tsx @@ -8,7 +8,7 @@ import { ResponsiveContainer, } from 'recharts'; import CustomChartTooltip from './CustomChartTooltip'; -import { useEffect, useMemo, useRef, useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; import Card from '@mui/material/Card'; import { Typography } from '@mui/material'; import Stack from '@mui/material/Stack'; @@ -25,28 +25,63 @@ import { } from '@services/api/use-graph-page-chart-data'; import { useGraphPageChartParams } from '@utils/hooks/use-graph-page-chart-params'; -const CHECKED_CHARTS_DEFAULT_STATE = { - transferAmount: true, - transactionsCount: true, - uniqueReceivers: true, - uniqueSenders: true, +export type GraphPageChartDataConfigObject = Partial< + Record +>; + +const CHECKED_CHARTS_DEFAULT_STATE: GraphPageChartDataConfigObject = { + totalTransactionAmount: true, + totalTransactionCount: true, + solved: true, + dailyUniqueReceivers: true, + dailyUniqueSenders: true, +}; +const HOVERED_CHARTS_DEFAULT_STATE: GraphPageChartDataConfigObject = { + totalTransactionAmount: false, + totalTransactionCount: false, + solved: false, + dailyUniqueReceivers: false, + dailyUniqueSenders: false, }; -const HOVERED_CHARTS_DEFAULT_STATE = { - transferAmount: false, - transactionsCount: false, - uniqueReceivers: false, - uniqueSenders: false, + +type SumOfNumericChartDataProperties = Record< + keyof Omit, + number +>; + +const sumNumericProperties = ( + chartData: GraphPageChartData +): SumOfNumericChartDataProperties => { + return chartData.reduce( + (acc, chartEntry) => { + acc.dailyUniqueReceivers += chartEntry.dailyUniqueReceivers; + acc.dailyUniqueSenders += chartEntry.dailyUniqueSenders; + acc.solved += chartEntry.solved; + acc.totalTransactionAmount += chartEntry.totalTransactionAmount; + acc.totalTransactionCount += chartEntry.totalTransactionCount; + return acc; + }, + { + dailyUniqueReceivers: 0, + dailyUniqueSenders: 0, + solved: 0, + totalTransactionAmount: 0, + totalTransactionCount: 0, + } + ); }; -export const AreaChart = ({ chartData }: { chartData: GraphPageChartData }) => { +export const AreaChart = () => { + const { data } = useGraphPageChartData(); + const chartData = data || []; const { setFromDate, setToDate, clearTimePeriod, dateRangeParams: { from, to }, + effectiveFromAllTimeDate, } = useGraphPageChartParams(); - - const lastIndex = chartData.length - 1; + const sum = sumNumericProperties(chartData); const [checkedCharts, setCheckedCharts] = useState( CHECKED_CHARTS_DEFAULT_STATE ); @@ -88,11 +123,14 @@ export const AreaChart = ({ chartData }: { chartData: GraphPageChartData }) => { event.preventDefault(); clearTimePeriod(); if (event.deltaY < 0) { - if (from.add(1, 'day').isAfter(to) || from.add(1, 'day').isSame(to)) { + if (from.add(1, 'day').isAfter(to)) { return; } setFromDate(from.add(1, 'day')); } else if (event.deltaY > 0) { + if (effectiveFromAllTimeDate?.isSame(from)) { + return; + } setFromDate(from.subtract(1, 'day')); } }; @@ -103,7 +141,7 @@ export const AreaChart = ({ chartData }: { chartData: GraphPageChartData }) => { currentRef.removeEventListener('wheel', handleScrollChangeDate); }; } - }, [clearTimePeriod, from, setFromDate, to]); + }, [clearTimePeriod, effectiveFromAllTimeDate, from, setFromDate, to]); return ( { > - + - - + @@ -135,14 +188,20 @@ export const AreaChart = ({ chartData }: { chartData: GraphPageChartData }) => { y2="1" > { y2="1" > { y2="1" > { y2="1" > @@ -214,11 +283,11 @@ export const AreaChart = ({ chartData }: { chartData: GraphPageChartData }) => { height={50} stroke={colorPalette.fog.dark} tickSize={20} - dataKey="name" + dataKey="date" tickMargin={10} /> } /> - {checkedCharts.transferAmount && ( + {checkedCharts.totalTransactionAmount && ( { fill="url(#colorTransferAmount)" /> )} - {checkedCharts.transactionsCount && ( + {checkedCharts.totalTransactionCount && ( { fill="url(#colorTransactionsCount)" /> )} - {checkedCharts.uniqueReceivers && ( + {checkedCharts.solved && ( + + )} + {checkedCharts.dailyUniqueReceivers && ( { fill="url(#colorUniqueRecievers)" /> )} - {checkedCharts.uniqueSenders && ( + {checkedCharts.dailyUniqueSenders && ( { title: 'Transfer Amount', isAreaChart: true, name: 'totalTransactionAmount', - amount: chartData[lastIndex]?.totalTransactionAmount, + amount: `${sum.totalTransactionAmount.toFixed()}`, color: colorPalette.primary.main, }, { title: 'Transactions Count', name: 'totalTransactionCount', - amount: chartData[lastIndex]?.totalTransactionCount, + amount: sum.totalTransactionCount, color: colorPalette.secondary.main, }, + { + title: 'Number of Tasks', + name: 'solved', + amount: sum.solved, + color: colorPalette.ocean.dark, + }, { title: 'Unique Receivers', name: 'dailyUniqueReceivers', - amount: chartData[lastIndex]?.dailyUniqueReceivers, + amount: sum.dailyUniqueReceivers, color: colorPalette.error.main, }, { title: 'Unique Senders', name: 'dailyUniqueSenders', - amount: chartData[lastIndex]?.dailyUniqueSenders, + amount: sum.dailyUniqueSenders, color: colorPalette.success.main, }, ]} diff --git a/packages/apps/dashboard/ui-2024/src/components/Charts/CustomChartTooltip.tsx b/packages/apps/dashboard/ui-2024/src/components/Charts/CustomChartTooltip.tsx index 881c3a7a37..8af64de1a8 100644 --- a/packages/apps/dashboard/ui-2024/src/components/Charts/CustomChartTooltip.tsx +++ b/packages/apps/dashboard/ui-2024/src/components/Charts/CustomChartTooltip.tsx @@ -1,20 +1,22 @@ import { TooltipProps } from 'recharts'; import Card from '@mui/material/Card'; import Box from '@mui/material/Box'; -import { Typography } from '@mui/material'; +import { Grid, Typography } from '@mui/material'; import Stack from '@mui/material/Stack'; import FiberManualRecordIcon from '@mui/icons-material/FiberManualRecord'; import { colorPalette } from '@assets/styles/color-palette'; import { formatDate } from '@helpers/formatDate'; +import { GraphPageChartDataConfigObject } from '@components/Charts/AreaChart'; const renderTitle = (title: string) => { - const currentTitle: Record = { - transferAmount: 'Transfer Amount', - transactionsCount: 'Transactions Count', - uniqueReceivers: 'Unique Receivers', - uniqueSenders: 'Unique Senders', + const currentTitle: GraphPageChartDataConfigObject = { + totalTransactionAmount: 'Transfer Amount', + totalTransactionCount: 'Transactions Count', + solved: 'Number of Tasks', + dailyUniqueReceivers: 'Unique Receivers', + dailyUniqueSenders: 'Unique Senders', }; - return currentTitle[title]; + return currentTitle[title as keyof GraphPageChartDataConfigObject]; }; const CustomChartTooltip = ({ @@ -22,17 +24,18 @@ const CustomChartTooltip = ({ label, active, }: TooltipProps) => { + console.log({ payload, label }); if (active) { return ( - {formatDate(label, 'MMMM d, YYYY')} + {formatDate(label, 'MMMM DD, YYYY')} {payload?.map((elem) => ( - - - - - {renderTitle(elem.name ?? '')} - + + + + + + {renderTitle(elem.name ?? '')} + + - - {elem.value} {elem.name === 'transferAmount' ? 'HMT' : ''} - - + + + {elem.value}{' '} + {elem.name === 'totalTransactionAmount' ? 'HMT' : ''} + + + ))} diff --git a/packages/apps/dashboard/ui-2024/src/components/Charts/ToggleCharts.tsx b/packages/apps/dashboard/ui-2024/src/components/Charts/ToggleCharts.tsx index 0ccdf7a883..47e9dcd3f2 100644 --- a/packages/apps/dashboard/ui-2024/src/components/Charts/ToggleCharts.tsx +++ b/packages/apps/dashboard/ui-2024/src/components/Charts/ToggleCharts.tsx @@ -62,18 +62,19 @@ const ToggleCharts = ({ {elem.title} {elem.amount ? elem.amount.toLocaleString('en-US') : ''} - {elem.name === 'transferAmount' && elem.isAreaChart && ( - - HMT - - )} + {elem.name === 'totalTransactionAmount' && + elem.isAreaChart && ( + + HMT + + )} } diff --git a/packages/apps/dashboard/ui-2024/src/components/DataEntry/DatePicker.tsx b/packages/apps/dashboard/ui-2024/src/components/DataEntry/DatePicker.tsx index f6f026298e..e74cb10e6b 100644 --- a/packages/apps/dashboard/ui-2024/src/components/DataEntry/DatePicker.tsx +++ b/packages/apps/dashboard/ui-2024/src/components/DataEntry/DatePicker.tsx @@ -80,9 +80,10 @@ const CustomDaterPicker = ({ props }: CustomDatePickerProps) => { interface DatePickerPropsMui { value: Dayjs; onChange: (value: Dayjs | null) => void; + customProps?: Omit['props']; } -const DatePicker = ({ value, onChange }: DatePickerPropsMui) => { +const DatePicker = ({ value, onChange, customProps }: DatePickerPropsMui) => { return ( { label: value.format('DD MMM, YYYY'), value: value, onChange: onChange, + ...customProps, }} /> diff --git a/packages/apps/dashboard/ui-2024/src/components/DataEntry/ToggleButtons.tsx b/packages/apps/dashboard/ui-2024/src/components/DataEntry/ToggleButtons.tsx index e56aa90f03..3c8f327344 100644 --- a/packages/apps/dashboard/ui-2024/src/components/DataEntry/ToggleButtons.tsx +++ b/packages/apps/dashboard/ui-2024/src/components/DataEntry/ToggleButtons.tsx @@ -4,7 +4,9 @@ import { styled } from '@mui/material'; import ToggleButtonGroup from '@mui/material/ToggleButtonGroup'; import { colorPalette } from '@assets/styles/color-palette'; import { + initialAllTime, TIME_PERIOD_OPTIONS, + TimePeriod, useGraphPageChartParams, } from '@utils/hooks/use-graph-page-chart-params'; @@ -18,8 +20,21 @@ export const StyledToggleButtonGroup = styled(ToggleButtonGroup)({ }); const ToggleButtons = () => { - const { setTimePeriod, selectedTimePeriod, dateRangeParams } = - useGraphPageChartParams(); + const { + setTimePeriod, + selectedTimePeriod, + dateRangeParams, + effectiveFromAllTimeDate, + } = useGraphPageChartParams(); + + const checkIfSelected = (element: TimePeriod) => { + if (element.name !== 'ALL' || !effectiveFromAllTimeDate) { + return element.value.isSame(dateRangeParams.from); + } + + return dateRangeParams.from.isSame(effectiveFromAllTimeDate); + }; + return ( { onClick={() => { setTimePeriod(elem); }} - selected={elem.value.isSame(dateRangeParams.from)} + selected={checkIfSelected(elem)} key={elem.name} sx={{ '.MuiTypography-root': { diff --git a/packages/apps/dashboard/ui-2024/src/pages/Graph/Graph.tsx b/packages/apps/dashboard/ui-2024/src/pages/Graph/Graph.tsx index c484050a41..c7408eed63 100644 --- a/packages/apps/dashboard/ui-2024/src/pages/Graph/Graph.tsx +++ b/packages/apps/dashboard/ui-2024/src/pages/Graph/Graph.tsx @@ -1,30 +1,20 @@ -import { LineChart, AreaChart } from '@components/Charts'; +import { AreaChart, LineChart } from '@components/Charts'; import Tabs from '@mui/material/Tabs'; import TabPanel from '@mui/lab/TabPanel'; import Tab from '@mui/material/Tab'; -import { useMemo, useState } from 'react'; +import { useState } from 'react'; import TabContext from '@mui/lab/TabContext'; import Typography from '@mui/material/Typography'; import PageWrapper from '@components/PageWrapper'; import Breadcrumbs from '@components/Breadcrumbs'; -import { useGraphPageChartData } from '@services/api/use-graph-page-chart-data'; type graphType = 'bucketed' | 'cumulative'; const Graph = () => { const [graphType, setGraphType] = useState('bucketed'); - const { data, isLoading, isError } = useGraphPageChartData(); - const memoizedData = useMemo(() => data, [data]); const handleGraphTypeChange = (_: unknown, newValue: graphType) => { setGraphType(newValue); }; - if (isLoading) { - return '...Loading'; - } - - if (isError) { - return 'Error'; - } return ( @@ -58,7 +48,7 @@ const Graph = () => { }} value="bucketed" > - + { - const result: GraphPageChartData = []; - // console.log({ hcaptchaStatsResults: , hmtStatsResults }); - - for (let i = 0; i < hcaptchaStatsResults.length; i++) { - result.push({ ...hcaptchaStatsResults[i], ...hmtStatsResults[i] }); - console.log(i); - } - return result; + const longerCollection = + hcaptchaStatsResults.length > hmtStatsResults.length + ? hcaptchaStatsResults + : hmtStatsResults; + + const hcaptchaStatsResultsMap = new Map(); + const hmtStatsResultsMap = new Map(); + + hcaptchaStatsResults.forEach((entry) => { + hcaptchaStatsResultsMap.set(entry.date, entry); + }); + + hmtStatsResults.forEach((entry) => { + hmtStatsResultsMap.set(entry.date, entry); + }); + + return longerCollection.map(({ date }) => { + const hmtStatsEntry: HMTDailyStat = hmtStatsResultsMap.get(date) || { + dailyUniqueReceivers: 0, + dailyUniqueSenders: 0, + date: date, + totalTransactionAmount: 0, + totalTransactionCount: 0, + }; + + const hcaptchaStatsEntry: HcaptchaDailyStat = hcaptchaStatsResultsMap.get( + date + ) || { + date: date, + served: 0, + solved: 0, + }; + + return { ...hmtStatsEntry, ...hcaptchaStatsEntry }; + }); }; const DEBOUNCE_MS = 300; export function useGraphPageChartData() { - const { dateRangeParams } = useGraphPageChartParams(); - const queryParams = { - from: dateRangeParams.from.format('YYYY-MM-DD'), - to: dateRangeParams.to.format('YYYY-MM-DD'), - }; + const { + dateRangeParams, + selectedTimePeriod, + effectiveFromAllTimeDate, + setEffectiveFromAllTimeDate, + setFromDate, + } = useGraphPageChartParams(); + const queryParams = useMemo( + () => ({ + from: dateRangeParams.from.format('YYYY-MM-DD'), + to: dateRangeParams.to.format('YYYY-MM-DD'), + }), + [dateRangeParams.from, dateRangeParams.to] + ); + const [debouncedQueryParams] = useDebounce(queryParams, DEBOUNCE_MS); return useQuery({ @@ -104,10 +143,26 @@ export function useGraphPageChartData() { hcaptchaDailyStatsResponseSchema ); - return mergeResponses( + const mergedResponses = mergeResponses( validHcaptchaGeneralStats.results, validHmtDailyStats.results ); + + const latestDate = mergedResponses[0]?.date + ? dayjs(new Date(validHcaptchaGeneralStats.results[0]?.date)) + : null; + + if ( + (selectedTimePeriod === 'ALL' && + !effectiveFromAllTimeDate && + latestDate) || + (!effectiveFromAllTimeDate && latestDate?.isAfter(dateRangeParams.from)) + ) { + setEffectiveFromAllTimeDate(latestDate); + setFromDate(latestDate); + } + + return mergedResponses; }, staleTime: DEBOUNCE_MS, queryKey: ['useGraphPageChartData', debouncedQueryParams], diff --git a/packages/apps/dashboard/ui-2024/src/utils/hooks/use-graph-page-chart-params.tsx b/packages/apps/dashboard/ui-2024/src/utils/hooks/use-graph-page-chart-params.tsx index 5b757d9c12..e933452f8c 100644 --- a/packages/apps/dashboard/ui-2024/src/utils/hooks/use-graph-page-chart-params.tsx +++ b/packages/apps/dashboard/ui-2024/src/utils/hooks/use-graph-page-chart-params.tsx @@ -12,7 +12,7 @@ const oneWeekAgo = dayjs().subtract(1, 'week'); const oneMonthAgo = dayjs().subtract(1, 'month'); const sixMonthsAgo = dayjs().subtract(6, 'months'); const oneYearAgo = dayjs().subtract(1, 'year'); -const allTime = dayjs().subtract(10, 'years'); +export const initialAllTime = dayjs().subtract(10, 'years'); export const TIME_PERIOD_OPTIONS: TimePeriod[] = [ { @@ -32,7 +32,7 @@ export const TIME_PERIOD_OPTIONS: TimePeriod[] = [ name: '1Y', }, { - value: allTime, + value: initialAllTime, name: 'ALL', }, ]; @@ -42,11 +42,13 @@ export interface GraphPageChartParams { from: Dayjs; to: Dayjs; }; + effectiveFromAllTimeDate?: Dayjs; selectedTimePeriod: GraphPageChartPeriodName | null; setTimePeriod: (timePeriod: TimePeriod) => void; clearTimePeriod: () => void; setFromDate: (fromDate: Dayjs | null) => void; setToDate: (toDate: Dayjs | null) => void; + setEffectiveFromAllTimeDate: (date: Dayjs) => void; } export const useGraphPageChartParams = create((set) => ({ @@ -54,18 +56,21 @@ export const useGraphPageChartParams = create((set) => ({ from: oneWeekAgo, to: dayjs(), }, + effectiveFromAllTimeDate: undefined, selectedTimePeriod: '1W', setFromDate: (fromDate: Dayjs | null) => { if (!fromDate) { - return null; + return; } - set((state) => ({ - ...state, - dateRangeParams: { - ...state.dateRangeParams, - from: fromDate, - }, - })); + set((state) => { + return { + ...state, + dateRangeParams: { + ...state.dateRangeParams, + from: fromDate, + }, + }; + }); }, setToDate: (toDate: Dayjs | null) => { if (!toDate) { @@ -80,16 +85,29 @@ export const useGraphPageChartParams = create((set) => ({ })); }, setTimePeriod: (timePeriod: TimePeriod) => { - set((state) => ({ - ...state, - selectedTimePeriod: timePeriod.name, - dateRangeParams: { - ...state.dateRangeParams, - from: timePeriod.value, - }, - })); + set((state) => { + const newFromDate = + state.effectiveFromAllTimeDate && timePeriod.name === 'ALL' + ? state.effectiveFromAllTimeDate + : timePeriod.value; + + return { + ...state, + selectedTimePeriod: timePeriod.name, + dateRangeParams: { + ...state.dateRangeParams, + from: newFromDate, + }, + }; + }); }, clearTimePeriod: () => { set((state) => ({ ...state, selectedTimePeriod: null })); }, + setEffectiveFromAllTimeDate: (date: Dayjs) => { + set((state) => ({ + ...state, + effectiveFromAllTimeDate: date, + })); + }, })); From fed83fd99b5a9da5a413724d9542d9eaff3dbe62 Mon Sep 17 00:00:00 2001 From: KacperKoza343 Date: Sat, 17 Aug 2024 19:24:05 +0200 Subject: [PATCH 3/4] feat(app/graph-page): fix chart details --- .../src/components/Charts/AreaChart.tsx | 28 +++++++++++++------ 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/packages/apps/dashboard/ui-2024/src/components/Charts/AreaChart.tsx b/packages/apps/dashboard/ui-2024/src/components/Charts/AreaChart.tsx index c361f56254..8f9b6fe7c8 100644 --- a/packages/apps/dashboard/ui-2024/src/components/Charts/AreaChart.tsx +++ b/packages/apps/dashboard/ui-2024/src/components/Charts/AreaChart.tsx @@ -228,6 +228,18 @@ export const AreaChart = () => { stopOpacity={currentHoveredChart.totalTransactionCount ? 1 : 0} /> + + + + { dataKey="totalTransactionAmount" stroke={colorPalette.primary.main} fillOpacity={1} - fill="url(#colorTransferAmount)" + fill="url(#colorTotalTransactionAmount)" /> )} {checkedCharts.totalTransactionCount && ( @@ -302,7 +314,7 @@ export const AreaChart = () => { dataKey="totalTransactionCount" stroke={colorPalette.secondary.main} fillOpacity={1} - fill="url(#colorTransactionsCount)" + fill="url(#colorTotalTransactionCount)" /> )} {checkedCharts.solved && ( @@ -311,16 +323,16 @@ export const AreaChart = () => { dataKey="solved" stroke={colorPalette.ocean.dark} fillOpacity={1} - fill="url(#colorTransactionsCount)" + fill="url(#colorSolved)" /> )} {checkedCharts.dailyUniqueReceivers && ( )} {checkedCharts.dailyUniqueSenders && ( @@ -329,7 +341,7 @@ export const AreaChart = () => { dataKey="dailyUniqueSenders" stroke={colorPalette.success.main} fillOpacity={1} - fill="url(#colorUniqueSenders)" + fill="url(#colorDailyUniqueSenders)" /> )} @@ -351,7 +363,7 @@ export const AreaChart = () => { title: 'Transfer Amount', isAreaChart: true, name: 'totalTransactionAmount', - amount: `${sum.totalTransactionAmount.toFixed()}`, + amount: `${Number(sum.totalTransactionAmount.toFixed())}`, color: colorPalette.primary.main, }, { @@ -370,7 +382,7 @@ export const AreaChart = () => { title: 'Unique Receivers', name: 'dailyUniqueReceivers', amount: sum.dailyUniqueReceivers, - color: colorPalette.error.main, + color: colorPalette.error.light, }, { title: 'Unique Senders', From 29e4e2d65dc5282a5c5ded5a6d4cf69a67bb4cc2 Mon Sep 17 00:00:00 2001 From: KacperKoza343 Date: Tue, 20 Aug 2024 14:45:00 +0200 Subject: [PATCH 4/4] fix(app/graph-page): fix chart data response and add optional scroll listener --- .../src/components/Charts/AreaChart.tsx | 18 ++++++++++++++++-- .../components/Charts/CustomChartTooltip.tsx | 1 - .../services/api/use-graph-page-chart-data.tsx | 2 -- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/packages/apps/dashboard/ui-2024/src/components/Charts/AreaChart.tsx b/packages/apps/dashboard/ui-2024/src/components/Charts/AreaChart.tsx index 8f9b6fe7c8..b3b817b65b 100644 --- a/packages/apps/dashboard/ui-2024/src/components/Charts/AreaChart.tsx +++ b/packages/apps/dashboard/ui-2024/src/components/Charts/AreaChart.tsx @@ -71,7 +71,11 @@ const sumNumericProperties = ( ); }; -export const AreaChart = () => { +export const AreaChart = ({ + changeDateOnScroll = false, +}: { + changeDateOnScroll?: boolean; +}) => { const { data } = useGraphPageChartData(); const chartData = data || []; const { @@ -117,6 +121,9 @@ export const AreaChart = () => { }; useEffect(() => { + if (!changeDateOnScroll) { + return; + } const currentRef = chartRef.current; if (currentRef) { const handleScrollChangeDate = (event: WheelEvent) => { @@ -141,7 +148,14 @@ export const AreaChart = () => { currentRef.removeEventListener('wheel', handleScrollChangeDate); }; } - }, [clearTimePeriod, effectiveFromAllTimeDate, from, setFromDate, to]); + }, [ + changeDateOnScroll, + clearTimePeriod, + effectiveFromAllTimeDate, + from, + setFromDate, + to, + ]); return ( ) => { - console.log({ payload, label }); if (active) { return (