Skip to content

Commit 48186b7

Browse files
authored
Merge pull request #2213 from pyth-network/cprussin/add-price-feed-chart
feat(insights): add publisher details page
2 parents 8b3fd2c + 4720ef3 commit 48186b7

File tree

80 files changed

+3224
-601
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

80 files changed

+3224
-601
lines changed

apps/insights/src/app/price-feeds/[slug]/price-components/[componentId]/page.tsx

Lines changed: 0 additions & 11 deletions
This file was deleted.

apps/insights/src/app/price-feeds/[slug]/price-components/layout.tsx

Lines changed: 0 additions & 1 deletion
This file was deleted.

apps/insights/src/app/price-feeds/[slug]/price-components/page.tsx

Lines changed: 0 additions & 3 deletions
This file was deleted.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { Publishers as default } from "../../../../components/PriceFeed/publishers";
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
export { PriceFeedsLayout as default } from "../../components/PriceFeeds/layout";
1+
export { ZoomLayoutTransition as default } from "../../components/ZoomLayoutTransition";
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
"use client";
2+
3+
export { Error as default } from "../../../components/Error";
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import type { Metadata } from "next";
2+
3+
export { PublishersLayout as default } from "../../../components/Publisher/layout";
4+
import { getPublishers } from "../../../services/clickhouse";
5+
6+
export const metadata: Metadata = {
7+
title: "Publishers",
8+
};
9+
10+
export const generateStaticParams = async () => {
11+
const publishers = await getPublishers();
12+
return publishers.map(({ key }) => ({ key }));
13+
};
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { Performance as default } from "../../../components/Publisher/performance";
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { PriceFeeds as default } from "../../../../components/Publisher/price-feeds";
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { ZoomLayoutTransition as default } from "../../components/ZoomLayoutTransition";
Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,11 @@
11
import type { NextRequest } from "next/server";
2-
import { z } from "zod";
32

4-
import { client } from "../../services/clickhouse";
3+
import { getYesterdaysPrices } from "../../services/clickhouse";
54

65
export async function GET(req: NextRequest) {
76
const symbols = req.nextUrl.searchParams.getAll("symbols");
8-
const rows = await client.query({
9-
query:
10-
"select symbol, price from insights_yesterdays_prices(symbols={symbols: Array(String)})",
11-
query_params: { symbols },
12-
});
13-
const result = await rows.json();
14-
const data = schema.parse(result.data);
7+
const data = await getYesterdaysPrices(symbols);
158
return Response.json(
169
Object.fromEntries(data.map(({ symbol, price }) => [symbol, price])),
1710
);
1811
}
19-
20-
const schema = z.array(
21-
z.object({
22-
symbol: z.string(),
23-
price: z.number(),
24-
}),
25-
);

apps/insights/src/components/ChangePercent/index.tsx

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

3-
import { CaretUp } from "@phosphor-icons/react/dist/ssr/CaretUp";
4-
import { Skeleton } from "@pythnetwork/component-library/Skeleton";
5-
import clsx from "clsx";
63
import { type ComponentProps, createContext, use } from "react";
74
import { useNumberFormatter } from "react-aria";
85
import { z } from "zod";
96

10-
import styles from "./index.module.scss";
117
import { StateType, useData } from "../../use-data";
8+
import { ChangeValue } from "../ChangeValue";
129
import { useLivePrice } from "../LivePrices";
1310

1411
const ONE_SECOND_IN_MS = 1000;
1512
const ONE_MINUTE_IN_MS = 60 * ONE_SECOND_IN_MS;
1613
const ONE_HOUR_IN_MS = 60 * ONE_MINUTE_IN_MS;
1714
const REFRESH_YESTERDAYS_PRICES_INTERVAL = ONE_HOUR_IN_MS;
1815

19-
const CHANGE_PERCENT_SKELETON_WIDTH = 15;
20-
2116
type Props = Omit<ComponentProps<typeof YesterdaysPricesContext>, "value"> & {
2217
feeds: (Feed & { symbol: string })[];
2318
};
@@ -92,12 +87,7 @@ export const ChangePercent = ({ feed, className }: ChangePercentProps) => {
9287

9388
case StateType.Loading:
9489
case StateType.NotLoaded: {
95-
return (
96-
<Skeleton
97-
className={clsx(styles.changePercent, className)}
98-
width={CHANGE_PERCENT_SKELETON_WIDTH}
99-
/>
100-
);
90+
return <ChangeValue className={className} isLoading />;
10191
}
10292

10393
case StateType.Loaded: {
@@ -107,7 +97,7 @@ export const ChangePercent = ({ feed, className }: ChangePercentProps) => {
10797
// eslint-disable-next-line unicorn/no-null
10898
return yesterdaysPrice === undefined ? null : (
10999
<ChangePercentLoaded
110-
className={clsx(styles.changePercent, className)}
100+
className={className}
111101
priorPrice={yesterdaysPrice}
112102
feed={feed}
113103
/>
@@ -130,7 +120,7 @@ const ChangePercentLoaded = ({
130120
const currentPrice = useLivePrice(feed);
131121

132122
return currentPrice === undefined ? (
133-
<Skeleton className={className} width={CHANGE_PERCENT_SKELETON_WIDTH} />
123+
<ChangeValue className={className} isLoading />
134124
) : (
135125
<PriceDifference
136126
className={className}
@@ -155,13 +145,12 @@ const PriceDifference = ({
155145
const direction = getDirection(currentPrice, priorPrice);
156146

157147
return (
158-
<span data-direction={direction} className={className}>
159-
<CaretUp weight="fill" className={styles.caret} />
148+
<ChangeValue direction={direction} className={className}>
160149
{numberFormatter.format(
161-
(100 * Math.abs(currentPrice - priorPrice)) / currentPrice,
150+
(100 * Math.abs(currentPrice - priorPrice)) / priorPrice,
162151
)}
163152
%
164-
</span>
153+
</ChangeValue>
165154
);
166155
};
167156

apps/insights/src/components/ChangePercent/index.module.scss renamed to apps/insights/src/components/ChangeValue/index.module.scss

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
@use "@pythnetwork/component-library/theme";
22

3-
.changePercent {
3+
.changeValue {
44
transition: color 100ms linear;
55
display: flex;
66
flex-flow: row nowrap;
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { CaretUp } from "@phosphor-icons/react/dist/ssr/CaretUp";
2+
import { Skeleton } from "@pythnetwork/component-library/Skeleton";
3+
import clsx from "clsx";
4+
import type { ComponentProps } from "react";
5+
6+
import styles from "./index.module.scss";
7+
8+
const SKELETON_WIDTH = 15;
9+
10+
type OwnProps =
11+
| { isLoading: true; skeletonWidth?: number | undefined }
12+
| {
13+
isLoading?: false;
14+
direction: "up" | "down" | "flat";
15+
};
16+
17+
type Props = Omit<ComponentProps<"span">, keyof OwnProps> & OwnProps;
18+
19+
export const ChangeValue = ({ className, children, ...props }: Props) => {
20+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
21+
const { isLoading, ...propsWithoutIsLoading } = props;
22+
return (
23+
<span
24+
className={clsx(styles.changeValue, className)}
25+
{...(!props.isLoading && { "data-direction": props.direction })}
26+
{...propsWithoutIsLoading}
27+
>
28+
<Contents {...props}>{children}</Contents>
29+
</span>
30+
);
31+
};
32+
33+
const Contents = (props: Props) => {
34+
if (props.isLoading) {
35+
return <Skeleton width={props.skeletonWidth ?? SKELETON_WIDTH} />;
36+
} else if (props.direction === "flat") {
37+
return "-";
38+
} else {
39+
return (
40+
<>
41+
<CaretUp weight="fill" className={styles.caret} />
42+
{props.children}
43+
</>
44+
);
45+
}
46+
};

apps/insights/src/components/CopyButton/index.tsx

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,27 +7,23 @@ import {
77
type Props as ButtonProps,
88
Button,
99
} from "@pythnetwork/component-library/Button";
10+
import type { Button as UnstyledButton } from "@pythnetwork/component-library/unstyled/Button";
1011
import clsx from "clsx";
11-
import { type ElementType, useCallback, useEffect, useState } from "react";
12+
import { useCallback, useEffect, useState } from "react";
1213

1314
import styles from "./index.module.scss";
1415

1516
type OwnProps = {
1617
text: string;
1718
};
1819

19-
type Props<T extends ElementType> = Omit<
20-
ButtonProps<T>,
20+
type Props = Omit<
21+
ButtonProps<typeof UnstyledButton>,
2122
keyof OwnProps | "onPress" | "afterIcon"
2223
> &
2324
OwnProps;
2425

25-
export const CopyButton = <T extends ElementType>({
26-
text,
27-
children,
28-
className,
29-
...props
30-
}: Props<T>) => {
26+
export const CopyButton = ({ text, children, className, ...props }: Props) => {
3127
const [isCopied, setIsCopied] = useState(false);
3228
const logger = useLogger();
3329
const copy = useCallback(() => {
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
"use client";
2+
3+
import { useMemo } from "react";
4+
import { useDateFormatter } from "react-aria";
5+
6+
type Props = Parameters<typeof useDateFormatter>[0] & {
7+
value: Date;
8+
};
9+
10+
export const FormattedDate = ({ value, ...args }: Props) => {
11+
const numberFormatter = useDateFormatter(args);
12+
return useMemo(() => numberFormatter.format(value), [numberFormatter, value]);
13+
};
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
@use "@pythnetwork/component-library/theme";
2+
3+
.meter {
4+
display: flex;
5+
flex-flow: column nowrap;
6+
gap: theme.spacing(2);
7+
8+
.labels {
9+
display: flex;
10+
flex-flow: row nowrap;
11+
justify-content: space-between;
12+
align-items: center;
13+
14+
@include theme.text("base", "medium");
15+
}
16+
17+
.score {
18+
height: theme.spacing(3);
19+
width: 100%;
20+
border-radius: theme.border-radius("full");
21+
position: relative;
22+
display: inline-block;
23+
background-color: theme.color("button", "outline", "background", "hover");
24+
25+
.fill {
26+
position: absolute;
27+
top: 0;
28+
bottom: 0;
29+
left: 0;
30+
border-radius: theme.border-radius("full");
31+
background: theme.color("chart", "series", "primary");
32+
}
33+
}
34+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
"use client";
2+
3+
import { Meter as MeterComponent } from "@pythnetwork/component-library/unstyled/Meter";
4+
import type { ComponentProps, ReactNode } from "react";
5+
6+
import styles from "./index.module.scss";
7+
8+
type OwnProps = {
9+
label: string;
10+
startLabel?: ReactNode | undefined;
11+
endLabel?: ReactNode | undefined;
12+
};
13+
type Props = Omit<ComponentProps<typeof MeterComponent>, keyof OwnProps> &
14+
OwnProps;
15+
16+
export const Meter = ({ label, startLabel, endLabel, ...props }: Props) => (
17+
<MeterComponent aria-label={label} {...props}>
18+
{({ percentage }) => (
19+
<div className={styles.meter}>
20+
{(startLabel !== undefined || endLabel !== undefined) && (
21+
<div className={styles.labels}>
22+
{startLabel ?? <div />}
23+
{endLabel ?? <div />}
24+
</div>
25+
)}
26+
<div className={styles.score}>
27+
<div
28+
className={styles.fill}
29+
style={{ width: `${percentage.toString()}%` }}
30+
/>
31+
</div>
32+
</div>
33+
)}
34+
</MeterComponent>
35+
);

0 commit comments

Comments
 (0)