Skip to content

Commit

Permalink
feat(insights): build header & tabs for price feed details page
Browse files Browse the repository at this point in the history
  • Loading branch information
cprussin committed Dec 11, 2024
1 parent 6b611c8 commit 51ada27
Show file tree
Hide file tree
Showing 66 changed files with 3,719 additions and 3,289 deletions.
2 changes: 1 addition & 1 deletion apps/api-reference/next-env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
/// <reference types="next/image-types/global" />

// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
2 changes: 1 addition & 1 deletion apps/insights/next-env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
/// <reference types="next/image-types/global" />

// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
2 changes: 1 addition & 1 deletion apps/insights/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
"clsx": "catalog:",
"cryptocurrency-icons": "catalog:",
"dnum": "catalog:",
"framer-motion": "catalog:",
"motion": "catalog:",
"next": "catalog:",
"next-themes": "catalog:",
"nuqs": "catalog:",
Expand Down
13 changes: 13 additions & 0 deletions apps/insights/src/app/price-feeds/[slug]/layout.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type { Metadata } from "next";

import { client } from "../../../services/pyth";
export { PriceFeedLayout as default } from "../../../components/PriceFeed/layout";

export const metadata: Metadata = {
title: "Price Feeds",
};

export const generateStaticParams = async () => {
const data = await client.getData();
return data.symbols.map((symbol) => ({ slug: encodeURIComponent(symbol) }));
};
1 change: 1 addition & 0 deletions apps/insights/src/app/price-feeds/[slug]/page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { Chart as default } from "../../../components/PriceFeed/chart";
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { PriceComponents as default } from "../../../../components/PriceFeed/price-components";
1 change: 1 addition & 0 deletions apps/insights/src/app/price-feeds/layout.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { PriceFeedsLayout as default } from "../../components/PriceFeeds/layout";
6 changes: 6 additions & 0 deletions apps/insights/src/app/price-feeds/page.ts
Original file line number Diff line number Diff line change
@@ -1 +1,7 @@
import type { Metadata } from "next";

export { PriceFeeds as default } from "../../components/PriceFeeds";

export const metadata: Metadata = {
title: "Price Feeds",
};
6 changes: 6 additions & 0 deletions apps/insights/src/app/publishers/page.ts
Original file line number Diff line number Diff line change
@@ -1 +1,7 @@
import type { Metadata } from "next";

export { Publishers as default } from "../../components/Publishers";

export const metadata: Metadata = {
title: "Publishers",
};
21 changes: 0 additions & 21 deletions apps/insights/src/components/AsyncValue/index.tsx

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
@use "@pythnetwork/component-library/theme";

.changePercent {
font-size: theme.font-size("sm");
transition: color 100ms linear;
display: flex;
flex-flow: row nowrap;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@

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

import styles from "./change-percent.module.scss";
import styles from "./index.module.scss";
import { StateType, useData } from "../../use-data";
import { useLivePrice } from "../LivePrices";

Expand All @@ -18,20 +19,17 @@ const REFRESH_YESTERDAYS_PRICES_INTERVAL = ONE_HOUR_IN_MS;
const CHANGE_PERCENT_SKELETON_WIDTH = 15;

type Props = Omit<ComponentProps<typeof YesterdaysPricesContext>, "value"> & {
symbolsToFeedKeys: Record<string, string>;
feeds: (Feed & { symbol: string })[];
};

const YesterdaysPricesContext = createContext<
undefined | ReturnType<typeof useData<Map<string, number>>>
>(undefined);

export const YesterdaysPricesProvider = ({
symbolsToFeedKeys,
...props
}: Props) => {
export const YesterdaysPricesProvider = ({ feeds, ...props }: Props) => {
const state = useData(
["yesterdaysPrices", Object.values(symbolsToFeedKeys)],
() => getYesterdaysPrices(symbolsToFeedKeys),
["yesterdaysPrices", feeds.map((feed) => feed.symbol)],
() => getYesterdaysPrices(feeds),
{
refreshInterval: REFRESH_YESTERDAYS_PRICES_INTERVAL,
},
Expand All @@ -41,17 +39,21 @@ export const YesterdaysPricesProvider = ({
};

const getYesterdaysPrices = async (
symbolsToFeedKeys: Record<string, string>,
feeds: (Feed & { symbol: string })[],
): Promise<Map<string, number>> => {
const url = new URL("/yesterdays-prices", window.location.origin);
for (const symbol of Object.keys(symbolsToFeedKeys)) {
url.searchParams.append("symbols", symbol);
for (const feed of feeds) {
url.searchParams.append("symbols", feed.symbol);
}
const response = await fetch(url);
const data: unknown = await response.json();
return new Map(
Object.entries(yesterdaysPricesSchema.parse(data)).map(
([symbol, value]) => [symbolsToFeedKeys[symbol] ?? "", value],
([symbol, value]) => [
feeds.find((feed) => feed.symbol === symbol)?.product.price_account ??
"",
value,
],
),
);
};
Expand All @@ -69,10 +71,17 @@ const useYesterdaysPrices = () => {
};

type ChangePercentProps = {
feedKey: string;
className?: string | undefined;
feed: Feed;
};

export const ChangePercent = ({ feedKey }: ChangePercentProps) => {
type Feed = {
product: {
price_account: string;
};
};

export const ChangePercent = ({ feed, className }: ChangePercentProps) => {
const yesterdaysPriceState = useYesterdaysPrices();

switch (yesterdaysPriceState.type) {
Expand All @@ -85,60 +94,68 @@ export const ChangePercent = ({ feedKey }: ChangePercentProps) => {
case StateType.NotLoaded: {
return (
<Skeleton
className={styles.changePercent}
className={clsx(styles.changePercent, className)}
width={CHANGE_PERCENT_SKELETON_WIDTH}
/>
);
}

case StateType.Loaded: {
const yesterdaysPrice = yesterdaysPriceState.data.get(feedKey);
const yesterdaysPrice = yesterdaysPriceState.data.get(
feed.product.price_account,
);
// eslint-disable-next-line unicorn/no-null
return yesterdaysPrice === undefined ? null : (
<ChangePercentLoaded priorPrice={yesterdaysPrice} feedKey={feedKey} />
<ChangePercentLoaded
className={clsx(styles.changePercent, className)}
priorPrice={yesterdaysPrice}
feed={feed}
/>
);
}
}
};

type ChangePercentLoadedProps = {
className?: string | undefined;
priorPrice: number;
feedKey: string;
feed: Feed;
};

const ChangePercentLoaded = ({
className,
priorPrice,
feedKey,
feed,
}: ChangePercentLoadedProps) => {
const currentPrice = useLivePrice(feedKey);
const currentPrice = useLivePrice(feed);

return currentPrice === undefined ? (
<Skeleton
className={styles.changePercent}
width={CHANGE_PERCENT_SKELETON_WIDTH}
/>
<Skeleton className={className} width={CHANGE_PERCENT_SKELETON_WIDTH} />
) : (
<PriceDifference
currentPrice={currentPrice.price}
className={className}
currentPrice={currentPrice.aggregate.price}
priorPrice={priorPrice}
/>
);
};

type PriceDifferenceProps = {
className?: string | undefined;
currentPrice: number;
priorPrice: number;
};

const PriceDifference = ({
className,
currentPrice,
priorPrice,
}: PriceDifferenceProps) => {
const numberFormatter = useNumberFormatter({ maximumFractionDigits: 2 });
const direction = getDirection(currentPrice, priorPrice);

return (
<span data-direction={direction} className={styles.changePercent}>
<span data-direction={direction} className={className}>
<CaretUp weight="fill" className={styles.caret} />
{numberFormatter.format(
(100 * Math.abs(currentPrice - priorPrice)) / currentPrice,
Expand Down
38 changes: 6 additions & 32 deletions apps/insights/src/components/CopyButton/index.module.scss
Original file line number Diff line number Diff line change
@@ -1,55 +1,29 @@
@use "@pythnetwork/component-library/theme";

.copyButton {
margin: -#{theme.spacing(0.5)} -#{theme.spacing(1)};
display: inline-block;
white-space: nowrap;
border-radius: theme.border-radius("md");
padding: theme.spacing(0.5) theme.spacing(1);
background: none;
cursor: pointer;
transition-property: background-color, color, border-color, outline-color;
transition-duration: 100ms;
transition-timing-function: linear;
border: 1px solid transparent;
outline-offset: 0;
outline: theme.spacing(1) solid transparent;

.iconContainer {
position: relative;
top: 0.125em;
margin-left: theme.spacing(1);
display: inline-block;

.copyIcon {
opacity: 0.5;
transition: opacity 100ms linear;
width: 1em;
height: 1em;
position: absolute;
inset: 0;
width: 100%;
height: 100%;
}

.checkIcon {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
color: theme.color("states", "success", "normal");
opacity: 0;
transition: opacity 100ms linear;
}
}

&[data-hovered] {
background-color: theme.color("button", "outline", "background", "hover");
}

&[data-pressed] {
background-color: theme.color("button", "outline", "background", "active");
}

&[data-focus-visible] {
border-color: theme.color("focus");
outline-color: theme.color("focus-dim");
}

&[data-is-copied] .iconContainer {
.copyIcon {
opacity: 0;
Expand Down
39 changes: 18 additions & 21 deletions apps/insights/src/components/CopyButton/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,23 @@
import { Check } from "@phosphor-icons/react/dist/ssr/Check";
import { Copy } from "@phosphor-icons/react/dist/ssr/Copy";
import { useLogger } from "@pythnetwork/app-logger";
import { UnstyledButton } from "@pythnetwork/component-library/UnstyledButton";
import { Button } from "@pythnetwork/component-library/Button";
import clsx from "clsx";
import { type ComponentProps, useCallback, useEffect, useState } from "react";

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

type CopyButtonProps = ComponentProps<typeof UnstyledButton> & {
type OwnProps = {
text: string;
};

export const CopyButton = ({
text,
children,
className,
...props
}: CopyButtonProps) => {
type Props = Omit<
ComponentProps<typeof Button>,
keyof OwnProps | "onPress" | "afterIcon"
> &
OwnProps;

export const CopyButton = ({ text, children, className, ...props }: Props) => {
const [isCopied, setIsCopied] = useState(false);
const logger = useLogger();
const copy = useCallback(() => {
Expand Down Expand Up @@ -52,23 +53,19 @@ export const CopyButton = ({
}, [isCopied]);

return (
<UnstyledButton
<Button
onPress={copy}
className={clsx(styles.copyButton, className)}
afterIcon={({ className, ...props }) => (
<div className={clsx(styles.iconContainer, className)} {...props}>
<Copy className={styles.copyIcon} />
<Check className={styles.checkIcon} />
</div>
)}
{...(isCopied && { "data-is-copied": true })}
{...props}
>
{(...args) => (
<>
<span>
{typeof children === "function" ? children(...args) : children}
</span>
<span className={styles.iconContainer}>
<Copy className={styles.copyIcon} />
<Check className={styles.checkIcon} />
</span>
</>
)}
</UnstyledButton>
{children}
</Button>
);
};
Loading

0 comments on commit 51ada27

Please sign in to comment.