Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { ChainTvlHistoricalChart } from "@geist/ui-react/components/defi/chain-tvl-historical-chart";
import type { Meta, StoryObj } from "@storybook/react";
import { expect, userEvent } from "@storybook/test";
import { withQueryClientProvider } from "#stories/decorators/wagmi.tsx";
import { setupCanvas } from "../utils/test-utils";

const meta = {
title: "DeFi/ChainTvlHistoricalChart",
component: ChainTvlHistoricalChart,
parameters: {
layout: "centered",
},
decorators: [withQueryClientProvider()],
} satisfies Meta<typeof ChainTvlHistoricalChart>;

export default meta;
type Story = StoryObj<typeof meta>;

export const EthereumMainnet: Story = {
args: {
chainId: "ethereum",
title: "Ethereum TVL",
color: "#5470FF",
},
play: async ({ canvasElement }: { canvasElement: HTMLElement }) => {
const { canvas } = await setupCanvas(canvasElement, 3000);

const chart = await canvas.findByTestId("chain-tvl-historical-chart");
await expect(chart).toBeInTheDocument();

const title = await canvas.findByText("Ethereum TVL");
await expect(title).toBeInTheDocument();

await userEvent.keyboard("{Tab}");

const tvlText = await canvas.findByText("TVL", undefined, {
timeout: 3000,
});
await expect(tvlText).toBeInTheDocument();

const dateText = await canvas.findByText("Date", undefined, {
timeout: 3000,
});
await expect(dateText).toBeInTheDocument();
},
};

export const ArbitrumOne: Story = {
args: {
chainId: "arbitrum",
title: "Arbitrum TVL",
color: "#28A0F0",
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { ProtocolTvlHistoricalChart } from "@geist/ui-react/components/defi/protocol-tvl-historical-chart";
import type { Meta, StoryObj } from "@storybook/react";
import { expect, userEvent } from "@storybook/test";
import { withQueryClientProvider } from "#stories/decorators/wagmi.tsx";
import { setupCanvas } from "../utils/test-utils";

const meta = {
title: "DeFi/ProtocolTvlHistoricalChart",
component: ProtocolTvlHistoricalChart,
parameters: {
layout: "centered",
},
decorators: [withQueryClientProvider()],
} satisfies Meta<typeof ProtocolTvlHistoricalChart>;

export default meta;
type Story = StoryObj<typeof meta>;

export const AaveProtocol: Story = {
args: {
protocol: "aave",
title: "Aave v2 TVL",
color: "#B6509E",
},
play: async ({ canvasElement }: { canvasElement: HTMLElement }) => {
const { canvas } = await setupCanvas(canvasElement, 3000);

const chart = await canvas.findByTestId("protocol-tvl-historical-chart");
await expect(chart).toBeInTheDocument();

const title = await canvas.findByText("Aave v2 TVL");
await expect(title).toBeInTheDocument();

await userEvent.keyboard("{Tab}");

const tvlText = await canvas.findByText("TVL", undefined, {
timeout: 3000,
});
await expect(tvlText).toBeInTheDocument();

const dateText = await canvas.findByText("Date", undefined, {
timeout: 3000,
});
await expect(dateText).toBeInTheDocument();
},
};

export const UniswapProtocol: Story = {
args: {
protocol: "uniswap",
title: "Uniswap v3 TVL",
color: "#FF007A",
},
};

export const MorphoProtocol: Story = {
args: {
protocol: "morpho",
title: "Morpho TVL",
color: "#00D395",
},
};
133 changes: 133 additions & 0 deletions packages/ui-react/src/components/defi/chain-tvl-historical-chart.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import { formatNumberWithLocale } from "@geist/domain/amount";
import { useQuery } from "@tanstack/react-query";
import { useMemo } from "react";
import { CartesianGrid, Line, LineChart, XAxis, YAxis } from "recharts";
import {
type ChartConfig,
ChartContainer,
ChartTooltip,
ChartTooltipContent,
} from "#components/shadcn/chart";
import { Skeleton } from "#components/shadcn/skeleton";

type ChainTvlHistoricalChartProps = {
chainId: string;
title?: string;
color?: string;
};

export type DefiLlamaTvlDataPoint = {
date: number;
tvl: number;
};

const chartConfig = {} satisfies ChartConfig;

async function getChainTvlHistoricalChartData(
chainId: string,
): Promise<DefiLlamaTvlDataPoint[]> {
const response = await fetch(
`https://api.llama.fi/v2/historicalChainTvl/${chainId}`,
);

if (!response.ok) {
throw new Error(`Failed to fetch TVL data: ${response.statusText}`);
}

const data = await response.json();
return data as DefiLlamaTvlDataPoint[];
}

export function ChainTvlHistoricalChart({
chainId,
title,
color,
}: ChainTvlHistoricalChartProps) {
const { data, isLoading } = useQuery({
queryKey: ["chain-tvl-historical-chart", chainId],
queryFn: () => getChainTvlHistoricalChartData(chainId),
});

const formatDate = (timestamp: number) => {
return new Date(timestamp * 1000).toLocaleDateString();
};

const formatTvl = (value: number) => {
return formatNumberWithLocale({
value,
locale: new Intl.Locale("en-US"),
formatOptions: {
style: "currency",
currency: "USD",
notation: "compact",
compactDisplay: "short",
},
});
};

const formattedTvlData = useMemo(() => {
if (!data) return [];

const mapTvlDataPoint = (dataPoint: DefiLlamaTvlDataPoint) => ({
date: dataPoint.date,
tvl: dataPoint.tvl,
});

const sortByDate = (a: DefiLlamaTvlDataPoint, b: DefiLlamaTvlDataPoint) =>
a.date - b.date;

return data.map(mapTvlDataPoint).sort(sortByDate);
}, [data]);

if (isLoading) return <Skeleton className="h-[400px] w-[600px]" />;

return (
<div
className="flex flex-col gap-2 items-center"
data-testid="chain-tvl-historical-chart"
>
{title && <div className="text-lg font-bold">{title}</div>}
<ChartContainer
config={chartConfig}
// cannot use tailwind here
style={{ height: "400px", width: "600px" }}
>
<LineChart accessibilityLayer data={formattedTvlData}>
<CartesianGrid strokeDasharray="3 3" horizontal vertical={false} />
<XAxis
dataKey="date"
type="number"
domain={["dataMin", "dataMax"]}
tickFormatter={formatDate}
label={{ value: "Date", position: "insideBottom", offset: -5 }}
/>
<YAxis
tickFormatter={formatTvl}
label={{ value: "TVL", position: "insideLeft", offset: -5 }}
/>
<ChartTooltip
content={
<ChartTooltipContent
formatter={(value, _name, props) => {
return (
<div className="text-sm flex flex-col gap-2">
<div>TVL: {formatTvl(value as number)}</div>
<div>Date: {formatDate(props.payload.date)}</div>
</div>
);
}}
/>
}
/>
<Line
type="monotone"
dataKey="tvl"
strokeWidth={2}
stroke={color}
dot={false}
/>
</LineChart>
</ChartContainer>
</div>
);
}
Loading
Loading