Skip to content

Commit c99f0f0

Browse files
authored
Merge pull request #2300 from pyth-network/cprussin/insights-landing
feat(insights): add overview page
2 parents 52c2def + 870e4f4 commit c99f0f0

File tree

17 files changed

+1087
-180
lines changed

17 files changed

+1087
-180
lines changed
Lines changed: 31 additions & 141 deletions
Original file line numberDiff line numberDiff line change
@@ -1,157 +1,47 @@
1-
"use client";
1+
import type { ComponentProps } from "react";
22

3-
import { type ComponentProps, createContext, use } from "react";
4-
import { useNumberFormatter } from "react-aria";
5-
import { z } from "zod";
6-
7-
import { StateType, useData } from "../../use-data";
83
import { ChangeValue } from "../ChangeValue";
9-
import { useLivePrice } from "../LivePrices";
10-
11-
const ONE_SECOND_IN_MS = 1000;
12-
const ONE_MINUTE_IN_MS = 60 * ONE_SECOND_IN_MS;
13-
const ONE_HOUR_IN_MS = 60 * ONE_MINUTE_IN_MS;
14-
const REFRESH_YESTERDAYS_PRICES_INTERVAL = ONE_HOUR_IN_MS;
15-
16-
type Props = Omit<ComponentProps<typeof YesterdaysPricesContext>, "value"> & {
17-
feeds: Record<string, string>;
18-
};
19-
20-
const YesterdaysPricesContext = createContext<
21-
undefined | ReturnType<typeof useData<Map<string, number>>>
22-
>(undefined);
23-
24-
export const YesterdaysPricesProvider = ({ feeds, ...props }: Props) => {
25-
const state = useData(
26-
["yesterdaysPrices", Object.keys(feeds)],
27-
() => getYesterdaysPrices(feeds),
28-
{
29-
refreshInterval: REFRESH_YESTERDAYS_PRICES_INTERVAL,
30-
},
31-
);
32-
33-
return <YesterdaysPricesContext value={state} {...props} />;
34-
};
35-
36-
const getYesterdaysPrices = async (
37-
feeds: Props["feeds"],
38-
): Promise<Map<string, number>> => {
39-
const url = new URL("/yesterdays-prices", window.location.origin);
40-
for (const symbol of Object.keys(feeds)) {
41-
url.searchParams.append("symbols", symbol);
42-
}
43-
const response = await fetch(url);
44-
const data = yesterdaysPricesSchema.parse(await response.json());
45-
return new Map(
46-
Object.entries(data).map(([symbol, value]) => [feeds[symbol] ?? "", value]),
47-
);
48-
};
49-
50-
const yesterdaysPricesSchema = z.record(z.string(), z.number());
51-
52-
const useYesterdaysPrices = () => {
53-
const state = use(YesterdaysPricesContext);
54-
55-
if (state) {
56-
return state;
57-
} else {
58-
throw new YesterdaysPricesNotInitializedError();
59-
}
60-
};
61-
62-
type ChangePercentProps = {
63-
className?: string | undefined;
64-
feedKey: string;
65-
};
66-
67-
export const ChangePercent = ({ feedKey, className }: ChangePercentProps) => {
68-
const yesterdaysPriceState = useYesterdaysPrices();
69-
70-
switch (yesterdaysPriceState.type) {
71-
case StateType.Error:
72-
case StateType.Loading:
73-
case StateType.NotLoaded: {
74-
return <ChangeValue className={className} isLoading />;
75-
}
76-
77-
case StateType.Loaded: {
78-
const yesterdaysPrice = yesterdaysPriceState.data.get(feedKey);
79-
return yesterdaysPrice === undefined ? (
80-
<ChangeValue className={className} isLoading />
81-
) : (
82-
<ChangePercentLoaded
83-
className={className}
84-
priorPrice={yesterdaysPrice}
85-
feedKey={feedKey}
86-
/>
87-
);
88-
}
89-
}
90-
};
4+
import { FormattedNumber } from "../FormattedNumber";
915

92-
type ChangePercentLoadedProps = {
6+
type PriceDifferenceProps = Omit<
7+
ComponentProps<typeof ChangeValue>,
8+
"children" | "direction" | "isLoading"
9+
> & {
9310
className?: string | undefined;
94-
priorPrice: number;
95-
feedKey: string;
96-
};
97-
98-
const ChangePercentLoaded = ({
99-
className,
100-
priorPrice,
101-
feedKey,
102-
}: ChangePercentLoadedProps) => {
103-
const { current } = useLivePrice(feedKey);
104-
105-
return current === undefined ? (
106-
<ChangeValue className={className} isLoading />
107-
) : (
108-
<PriceDifference
109-
className={className}
110-
currentPrice={current.aggregate.price}
111-
priorPrice={priorPrice}
112-
/>
11+
} & (
12+
| { isLoading: true }
13+
| {
14+
isLoading?: false;
15+
currentValue: number;
16+
previousValue: number;
17+
}
11318
);
114-
};
115-
116-
type PriceDifferenceProps = {
117-
className?: string | undefined;
118-
currentPrice: number;
119-
priorPrice: number;
120-
};
12119

122-
const PriceDifference = ({
123-
className,
124-
currentPrice,
125-
priorPrice,
126-
}: PriceDifferenceProps) => {
127-
const numberFormatter = useNumberFormatter({ maximumFractionDigits: 2 });
128-
const direction = getDirection(currentPrice, priorPrice);
129-
130-
return (
131-
<ChangeValue direction={direction} className={className}>
132-
{numberFormatter.format(
133-
(100 * Math.abs(currentPrice - priorPrice)) / priorPrice,
134-
)}
20+
export const ChangePercent = ({ ...props }: PriceDifferenceProps) =>
21+
props.isLoading ? (
22+
<ChangeValue {...props} />
23+
) : (
24+
<ChangeValue
25+
direction={getDirection(props.currentValue, props.previousValue)}
26+
{...props}
27+
>
28+
<FormattedNumber
29+
maximumFractionDigits={2}
30+
value={
31+
(100 * Math.abs(props.currentValue - props.previousValue)) /
32+
props.previousValue
33+
}
34+
/>
13535
%
13636
</ChangeValue>
13737
);
138-
};
13938

140-
const getDirection = (currentPrice: number, priorPrice: number) => {
141-
if (currentPrice < priorPrice) {
39+
const getDirection = (currentValue: number, previousValue: number) => {
40+
if (currentValue < previousValue) {
14241
return "down";
143-
} else if (currentPrice > priorPrice) {
42+
} else if (currentValue > previousValue) {
14443
return "up";
14544
} else {
14645
return "flat";
14746
}
14847
};
149-
150-
class YesterdaysPricesNotInitializedError extends Error {
151-
constructor() {
152-
super(
153-
"This component must be contained within a <YesterdaysPricesProvider>",
154-
);
155-
this.name = "YesterdaysPricesNotInitializedError";
156-
}
157-
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
@use "@pythnetwork/component-library/theme";
2+
3+
.chartCard {
4+
.line {
5+
color: theme.color("chart", "series", "neutral");
6+
}
7+
8+
&[data-variant="primary"] {
9+
.line {
10+
color: theme.color("chart", "series", "primary");
11+
}
12+
}
13+
}

apps/insights/src/components/Publisher/chart-card.tsx renamed to apps/insights/src/components/ChartCard/index.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"use client";
22

33
import { StatCard } from "@pythnetwork/component-library/StatCard";
4+
import clsx from "clsx";
45
import dynamic from "next/dynamic";
56
import {
67
type ElementType,
@@ -14,6 +15,8 @@ import {
1415
import { ResponsiveContainer, Tooltip, Line, XAxis, YAxis } from "recharts";
1516
import type { CategoricalChartState } from "recharts/types/chart/types";
1617

18+
import styles from "./index.module.scss";
19+
1720
const LineChart = dynamic(
1821
() => import("recharts").then((recharts) => recharts.LineChart),
1922
{
@@ -25,7 +28,6 @@ const CHART_HEIGHT = 36;
2528

2629
type OwnProps<T> = {
2730
chartClassName?: string | undefined;
28-
lineClassName?: string | undefined;
2931
data: Point<T>[];
3032
};
3133

@@ -43,8 +45,8 @@ type Props<T extends ElementType, U> = Omit<
4345
OwnProps<U>;
4446

4547
export const ChartCard = <T extends ElementType, U>({
48+
className,
4649
chartClassName,
47-
lineClassName,
4850
data,
4951
stat,
5052
miniStat,
@@ -77,6 +79,7 @@ export const ChartCard = <T extends ElementType, U>({
7779

7880
return (
7981
<StatCard
82+
className={clsx(className, styles.chartCard)}
8083
{...props}
8184
stat={selectedPoint ? (selectedPoint.displayY ?? selectedPoint.y) : stat}
8285
miniStat={selectedDate ?? miniStat}
@@ -96,7 +99,7 @@ export const ChartCard = <T extends ElementType, U>({
9699
<Line
97100
type="monotone"
98101
dataKey="y"
99-
className={lineClassName ?? ""}
102+
className={styles.line ?? ""}
100103
stroke="currentColor"
101104
dot={false}
102105
/>

apps/insights/src/components/Overview/index.module.scss

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,59 @@
88

99
color: theme.color("heading");
1010
font-weight: theme.font-weight("semibold");
11+
margin-bottom: theme.spacing(6);
12+
}
13+
14+
.stats {
15+
display: flex;
16+
flex-flow: row nowrap;
17+
align-items: stretch;
18+
gap: theme.spacing(6);
19+
20+
& > * {
21+
flex: 1 1 0px;
22+
width: 0;
23+
}
24+
25+
.publishersChart,
26+
.priceFeedsChart {
27+
& svg {
28+
cursor: pointer;
29+
}
30+
}
31+
}
32+
33+
.overviewMainContent {
34+
display: grid;
35+
grid-template-columns: repeat(2, 1fr);
36+
gap: theme.spacing(40);
37+
align-items: center;
38+
padding: theme.spacing(18) 0;
39+
40+
.headline {
41+
@include theme.text("3xl", "medium");
42+
43+
color: theme.color("heading");
44+
line-height: 125%;
45+
margin-top: theme.spacing(8);
46+
margin-bottom: theme.spacing(4);
47+
}
48+
49+
.message {
50+
@include theme.text("base", "normal");
51+
52+
color: theme.color("heading");
53+
line-height: 150%;
54+
}
55+
56+
.tabList {
57+
margin: theme.spacing(12) 0;
58+
}
59+
60+
.buttons {
61+
display: flex;
62+
flex-flow: row nowrap;
63+
gap: theme.spacing(3);
64+
}
1165
}
1266
}

0 commit comments

Comments
 (0)