diff --git a/src/hooks/useAffiliatesInfo.ts b/src/hooks/useAffiliatesInfo.ts index 97ef3d0c4..b83225cea 100644 --- a/src/hooks/useAffiliatesInfo.ts +++ b/src/hooks/useAffiliatesInfo.ts @@ -10,10 +10,12 @@ type AffiliatesMetadata = { isAffiliate: boolean; }; +process.env.VITE_AFFILIATES_SERVER_BASE_URL = 'http://localhost:3000'; + export const useAffiliatesInfo = (dydxAddress?: string) => { const { compositeClient, getAffiliateInfo } = useDydxClient(); - const queryFn = async () => { + const fetchAffiliateMetadata = async () => { if (!compositeClient || !dydxAddress) { return {}; } @@ -38,11 +40,95 @@ export const useAffiliatesInfo = (dydxAddress?: string) => { } }; - const query = useQuery({ - queryKey: ['affiliatesMetadata', dydxAddress], - queryFn, + const fetchProgramStats = async () => { + const endpoint = `${process.env.VITE_AFFILIATES_SERVER_BASE_URL}/v1/community/program-stats`; + + try { + const res = await fetch(endpoint, { + headers: { + 'Content-Type': 'application/json', + }, + method: 'GET', + }); + + const data = await res.json(); + return data; + } catch (error) { + log('useAffiliatesInfo/fetchProgramStats', error, { endpoint }); + throw error; + } + }; + + const fetchAccountStats = async () => { + // if (!isConnectedWagmi) { + // return; + // } + const endpoint = `${process.env.VITE_AFFILIATES_SERVER_BASE_URL}/v1/leaderboard/account/${dydxAddress}`; + + try { + const res = await fetch(endpoint, { + headers: { + 'Content-Type': 'application/json', + }, + method: 'GET', + }); + + const data = await res.json(); + + return data; + } catch (error) { + log('useAffiliatesInfo/fetchAccountStats', error, { endpoint }); + throw error; + } + }; + + const fetchLastUpdated = async () => { + const endpoint = `${process.env.VITE_AFFILIATES_SERVER_BASE_URL}/v1/last-updated`; + + try { + const res = await fetch(endpoint, { + headers: { + 'Content-Type': 'application/json', + }, + method: 'GET', + }); + + const data = await res.json(); + + return data; + } catch (error) { + log('useAffiliatesInfo/fetchLastUpdated', error, { endpoint }); + throw error; + } + }; + + const affiliateMetadataQuery = useQuery({ + queryKey: ['affiliateMetadata', dydxAddress], + queryFn: fetchAffiliateMetadata, enabled: Boolean(compositeClient && dydxAddress), }); - return query; + const programStatsQuery = useQuery({ + queryKey: ['programStats'], + queryFn: fetchProgramStats, + enabled: Boolean(compositeClient), + }); + + const affiliateStatsQuery = useQuery({ + queryKey: ['accountStats', dydxAddress], + queryFn: fetchAccountStats, + enabled: Boolean(dydxAddress), + }); + + const lastUpdatedQuery = useQuery({ + queryKey: ['lastUpdated'], + queryFn: fetchLastUpdated, + }); + + return { + affiliateMetadataQuery, + programStatsQuery, + affiliateStatsQuery, + lastUpdatedQuery, + }; }; diff --git a/src/hooks/useAffiliatesLeaderboard.ts b/src/hooks/useAffiliatesLeaderboard.ts new file mode 100644 index 000000000..62f4b604e --- /dev/null +++ b/src/hooks/useAffiliatesLeaderboard.ts @@ -0,0 +1,41 @@ +import { useEffect, useState } from 'react'; + +import { IAffiliateStats } from '@/constants/affiliates'; + +import { log } from '@/lib/telemetry'; + +export const useAffiliatesLeaderboard = () => { + const [page, setPage] = useState(1); + const [total, setTotal] = useState(0); + const [affiliates, setAffiliates] = useState([]); + + useEffect(() => { + fetchAffiliateStats(); + }, [page]); + + const fetchAffiliateStats = async () => { + process.env.VITE_AFFILIATES_SERVER_BASE_URL = 'http://localhost:3000'; + + const endpoint = `${process.env.VITE_AFFILIATES_SERVER_BASE_URL}/v1/leaderboard/search`; + + try { + const response = await fetch(endpoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ pagination: { page, pageSize: 10 } }), + }); + + const data = await response.json(); + + setAffiliates((prev) => [...prev, ...data.results]); + setTotal(data.total); + } catch (error) { + log('useAffiliateLeaderboard', error, { endpoint }); + throw error; + } + }; + + return { affiliates, total, page, setPage }; +}; diff --git a/src/hooks/useCommunityChart.ts b/src/hooks/useCommunityChart.ts new file mode 100644 index 000000000..2ab4c8aed --- /dev/null +++ b/src/hooks/useCommunityChart.ts @@ -0,0 +1,68 @@ +import { useState } from 'react'; + +import { useQuery } from '@tanstack/react-query'; + +import { IDateStats } from '@/constants/affiliates'; +import { AffiliatesProgramMetric, AffiliatesProgramPeriod } from '@/constants/charts'; + +import { log } from '@/lib/telemetry'; + +export const useCommunityChart = (selectedChartMetric: AffiliatesProgramMetric) => { + const [selectedPeriod, setSelectedPeriod] = useState( + AffiliatesProgramPeriod.PeriodAllTime + ); + + const getStartDate = (): string => { + const currentTime = new Date(); + + switch (selectedPeriod) { + case AffiliatesProgramPeriod.Period1d: + return new Date(currentTime.setDate(currentTime.getDate() - 1)).toISOString(); + case AffiliatesProgramPeriod.Period7d: + return new Date(currentTime.setDate(currentTime.getDate() - 7)).toISOString(); + case AffiliatesProgramPeriod.Period30d: + return new Date(currentTime.setMonth(currentTime.getMonth() - 1)).toISOString(); + case AffiliatesProgramPeriod.Period90d: + return new Date(currentTime.setMonth(currentTime.getMonth() - 3)).toISOString(); + case AffiliatesProgramPeriod.PeriodAllTime: + return new Date(0).toISOString(); // The earliest possible date + default: + throw new Error('Invalid rolling window value'); + } + }; + + const fetchCommunityChartMetrics = async () => { + process.env.VITE_AFFILIATES_SERVER_BASE_URL = 'http://localhost:3000'; + const endpoint = `${process.env.VITE_AFFILIATES_SERVER_BASE_URL}/v1/community/chart-metrics?start_date=${getStartDate()}&end_date=${new Date().toISOString()}`; + + try { + const response = await fetch(endpoint, { + headers: { + 'Content-Type': 'application/json', + }, + method: 'GET', + }); + + const data: IDateStats[] = await response.json(); + + const result = data + .sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()) + .map((m) => ({ + date: new Date(m.date).getTime(), + cumulativeAmount: Number(m[selectedChartMetric]), + })); + + return result; + } catch (error) { + log('useAffiliatesCommunityChart', error, { endpoint }); + throw error; + } + }; + + const communityChartMetricsQuery = useQuery({ + queryKey: ['communityChart'], + queryFn: fetchCommunityChartMetrics, + }); + + return { communityChartMetricsQuery, selectedPeriod, setSelectedPeriod }; +}; diff --git a/src/pages/affiliates/AffiliatesPage.tsx b/src/pages/affiliates/AffiliatesPage.tsx index f4024fad2..cc8809ec7 100644 --- a/src/pages/affiliates/AffiliatesPage.tsx +++ b/src/pages/affiliates/AffiliatesPage.tsx @@ -1,6 +1,5 @@ -import React, { Suspense, useEffect, useState } from 'react'; +import React, { Suspense, useState } from 'react'; -import axios from 'axios'; import { Navigate, Route, Routes } from 'react-router-dom'; import styled from 'styled-components'; @@ -9,6 +8,7 @@ import { STRING_KEYS } from '@/constants/localization'; import { AffiliateRoute } from '@/constants/routes'; import { useAccounts } from '@/hooks/useAccounts'; +import { useAffiliatesInfo } from '@/hooks/useAffiliatesInfo'; import { useBreakpoints } from '@/hooks/useBreakpoints'; import { useStringGetter } from '@/hooks/useStringGetter'; import { useWalletConnection } from '@/hooks/useWalletConnection'; @@ -58,49 +58,22 @@ const $NavigationMenu = styled(NavigationMenu)` export const AffiliatesPage: React.FC = () => { const { isConnectedWagmi } = useWalletConnection(); const { dydxAddress } = useAccounts(); + const { programStatsQuery, affiliateStatsQuery, lastUpdatedQuery, affiliateMetadataQuery } = + useAffiliatesInfo(dydxAddress); + const { data: lastUpdated } = lastUpdatedQuery; + const { data: accountStats } = affiliateStatsQuery; + const { data: programStats } = programStatsQuery; + const { data: affiliateMetadata } = affiliateMetadataQuery; const { isNotTablet } = useBreakpoints(); const stringGetter = useStringGetter(); - const [accountStats, setAccountStats] = useState(); - const [programStats, setProgramStats] = useState(); - const [lastUpdated, setLastUpdated] = useState(); const [currTab, setCurrTab] = useState(AffiliateRoute.Leaderboard); // Mocked user status data const userStatus = { - isAffiliate: true, - isVip: true, - currentAffiliateTier: 2, - }; - - useEffect(() => { - fetchAccountStats(); - }, [isConnectedWagmi]); - - useEffect(() => { - fetchProgramStats(); - fetchLastUpdated(); - }, []); - - const fetchProgramStats = async () => { - const res = await axios.get(`http://localhost:3000/v1/community/program-stats`); - setProgramStats(res.data); - }; - - const fetchAccountStats = async () => { - if (!isConnectedWagmi) { - setAccountStats(undefined); - return; - } - - const res = await axios.get(`http://localhost:3000/v1/leaderboard/account/${dydxAddress}`); - - setAccountStats(res.data); - }; - - const fetchLastUpdated = async () => { - const res = await axios.get(`http://localhost:3000/v1/last-updated`); - setLastUpdated(res.data); + isAffiliate: affiliateMetadata?.metadata?.isAffiliate ?? false, + isVip: affiliateMetadata?.affiliateInfo?.isWhitelisted ?? false, + currentAffiliateTier: affiliateMetadata?.affiliateInfo?.tier ?? undefined, }; const routesComponent = ( diff --git a/src/views/Affiliates/AffiliatesLeaderboard.tsx b/src/views/Affiliates/AffiliatesLeaderboard.tsx index 74c2f1a6e..d6c8d61d9 100644 --- a/src/views/Affiliates/AffiliatesLeaderboard.tsx +++ b/src/views/Affiliates/AffiliatesLeaderboard.tsx @@ -1,6 +1,5 @@ -import { useEffect, useState } from 'react'; +import { useState } from 'react'; -import axios from 'axios'; import styled, { css } from 'styled-components'; import tw from 'twin.macro'; @@ -8,6 +7,7 @@ import { IAffiliateStats } from '@/constants/affiliates'; import { ButtonAction } from '@/constants/buttons'; import { STRING_KEYS } from '@/constants/localization'; +import { useAffiliatesLeaderboard } from '@/hooks/useAffiliatesLeaderboard'; import { useBreakpoints } from '@/hooks/useBreakpoints'; import { useStringGetter } from '@/hooks/useStringGetter'; @@ -93,28 +93,8 @@ export const AffiliatesLeaderboard = ({ const { isTablet } = useBreakpoints(); const stringGetter = useStringGetter(); const affiliatesFilters = Object.values(AffiliateEpochsFilter); - const [affiliates, setAffiliates] = useState([]); const [epochFilter, setEpochFilter] = useState(AffiliateEpochsFilter.ALL); - const [page, setPage] = useState(1); - const [total, setTotal] = useState(0); - - useEffect(() => { - fetchAffiliateStats(); - }, [page]); - - const fetchAffiliateStats = async () => { - // Comment for testing with local data - const response = await axios.post('http://localhost:3000/v1/leaderboard/search', { - pagination: { - page, - pageSize: 10, // Amount of entities to load - }, - }); - - setAffiliates([...affiliates, ...response.data.results]); - - setTotal(response.data.total); - }; + const { affiliates, total, setPage } = useAffiliatesLeaderboard(); const handleLoadMore = () => { setPage((prev) => prev + 1); diff --git a/src/views/Affiliates/cards/AffiliateStatsCard.tsx b/src/views/Affiliates/cards/AffiliateStatsCard.tsx index 1620e6bb1..5968f8fe2 100644 --- a/src/views/Affiliates/cards/AffiliateStatsCard.tsx +++ b/src/views/Affiliates/cards/AffiliateStatsCard.tsx @@ -23,7 +23,7 @@ const MobileView = ({ accountStats?: IAffiliateStats; toggleCriteria: () => void; isVip: boolean; - currentAffiliateTier: number; + currentAffiliateTier?: number; }) => { const stringGetter = useStringGetter(); @@ -99,7 +99,7 @@ const DesktopView = ({ accountStats?: IAffiliateStats; toggleCriteria: () => void; isVip: boolean; - currentAffiliateTier: number; + currentAffiliateTier?: number; }) => { const stringGetter = useStringGetter(); const linkRef = useRef(null); // Reference to the button element @@ -171,7 +171,7 @@ interface IAffiliateStatsProps { className?: string; accountStats?: IAffiliateStats; isVip: boolean; - currentAffiliateTier: number; + currentAffiliateTier?: number; } export const AffiliateStatsCard = ({ @@ -207,7 +207,7 @@ export const AffiliateStatsCard = ({ {isCriteriaVisible && ( { className="relative" title={stringGetter({ key: STRING_KEYS.AFFILIATE_EARNINGS })} outputType={OutputType.CompactFiat} - value={programStats.totalEarnings} + value={programStats?.totalEarnings} />
@@ -49,27 +49,27 @@ const MobileView = ({ programStats }: { programStats: IProgramStats }) => { className="w-6/12 p-1" title={stringGetter({ key: STRING_KEYS.VOLUME_REFERRED })} outputType={OutputType.CompactFiat} - value={programStats.referredVolume} + value={programStats?.referredVolume} />
@@ -87,14 +87,14 @@ const DesktopView = ({ programStats }: { programStats: IProgramStats }) => { className="relative inline-block" title={stringGetter({ key: STRING_KEYS.AFFILIATE_EARNINGS })} outputType={OutputType.CompactFiat} - value={programStats.totalEarnings} + value={programStats?.totalEarnings} />
@@ -103,27 +103,27 @@ const DesktopView = ({ programStats }: { programStats: IProgramStats }) => { className="pr-1" outputType={OutputType.CompactFiat} title={stringGetter({ key: STRING_KEYS.VOLUME_REFERRED })} - value={programStats.referredVolume} + value={programStats?.referredVolume} />
diff --git a/src/views/Affiliates/community-chart/ProgramHistoricalChart.tsx b/src/views/Affiliates/community-chart/ProgramHistoricalChart.tsx index 39c58a443..65d73f108 100644 --- a/src/views/Affiliates/community-chart/ProgramHistoricalChart.tsx +++ b/src/views/Affiliates/community-chart/ProgramHistoricalChart.tsx @@ -2,11 +2,9 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { curveLinear } from '@visx/curve'; import { TooltipContextType } from '@visx/xychart'; -import axios from 'axios'; import { debounce } from 'lodash'; import styled from 'styled-components'; -import { IDateStats } from '@/constants/affiliates'; import { AffiliatesProgramDatum, AffiliatesProgramMetric, @@ -18,6 +16,7 @@ import { STRING_KEYS } from '@/constants/localization'; import { TOKEN_DECIMALS } from '@/constants/numbers'; import { timeUnits } from '@/constants/time'; +import { useCommunityChart } from '@/hooks/useCommunityChart'; import { useEnvConfig } from '@/hooks/useEnvConfig'; import { useLocaleSeparators } from '@/hooks/useLocaleSeparators'; import { useNow } from '@/hooks/useNow'; @@ -72,11 +71,11 @@ export const ProgramHistoricalChart = ({ const [tooltipContext, setTooltipContext] = useState>(); const [isZooming, setIsZooming] = useState(false); - const [selectedPeriod, setSelectedPeriod] = useState( - AffiliatesProgramPeriod.PeriodAllTime - ); + const [defaultZoomDomain, setDefaultZoomDomain] = useState(undefined); - const [metricData, setMetricData] = useState<{ date: number; cumulativeAmount: number }[]>([]); + const { setSelectedPeriod, selectedPeriod, communityChartMetricsQuery } = + useCommunityChart(selectedChartMetric); + const metricData = communityChartMetricsQuery?.data ?? []; const chartTitles = { [AffiliatesProgramMetric.AffiliateEarnings]: stringGetter({ @@ -87,48 +86,9 @@ export const ProgramHistoricalChart = ({ [AffiliatesProgramMetric.ReferredVolume]: stringGetter({ key: STRING_KEYS.VOLUME_REFERRED }), }; - useEffect(() => { - fetchMetricData(); - }, [selectedChartMetric, selectedPeriod]); - - const getStartDate = (): string => { - const currentTime = new Date(); - - switch (selectedPeriod) { - case AffiliatesProgramPeriod.Period1d: - return new Date(currentTime.setDate(currentTime.getDate() - 1)).toISOString(); - case AffiliatesProgramPeriod.Period7d: - return new Date(currentTime.setDate(currentTime.getDate() - 7)).toISOString(); - case AffiliatesProgramPeriod.Period30d: - return new Date(currentTime.setMonth(currentTime.getMonth() - 1)).toISOString(); - case AffiliatesProgramPeriod.Period90d: - return new Date(currentTime.setMonth(currentTime.getMonth() - 3)).toISOString(); - case AffiliatesProgramPeriod.PeriodAllTime: - return new Date(0).toISOString(); // The earliest possible date - default: - throw new Error('Invalid rolling window value'); - } - }; - - const fetchMetricData = async () => { - const { data } = await axios.get( - `http://localhost:3000/v1/community/chart-metrics?start_date=${getStartDate()}&end_date=${new Date().toISOString()}` - ); - - const periodData: IDateStats[] = data; - - setMetricData( - periodData - .sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()) - .map((m) => ({ - date: new Date(m.date).getTime(), - cumulativeAmount: Number(m[selectedChartMetric]), - })) - ); - }; - - const oldestDataPointDate = metricData?.[0]?.date; - const newestDataPointDate = metricData?.[metricData.length - 1]?.date; + const oldestDataPointDate = metricData[0] && new Date(metricData[0].date).getTime(); + const newestDataPointDate = + metricData[metricData.length - 1] && new Date(metricData.length - 1).getTime(); const msForPeriod = useCallback( (period: AffiliatesProgramPeriod, clampMax: Boolean = true) => { @@ -153,26 +113,6 @@ export const ProgramHistoricalChart = ({ [now, historyStartDate, newestDataPointDate, oldestDataPointDate] ); - // 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): AffiliatesProgramPeriod[] => - // affiliatesProgramPeriods.reduce((acc: AffiliatesProgramPeriod[], 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( () => diff --git a/src/views/dialogs/ReferralDialog.tsx b/src/views/dialogs/ReferralDialog.tsx index bf57ca206..484777359 100644 --- a/src/views/dialogs/ReferralDialog.tsx +++ b/src/views/dialogs/ReferralDialog.tsx @@ -42,8 +42,10 @@ export const ReferralDialog = ({ setIsOpen, refCode }: DialogProps { if (referralAddress) {