Skip to content

Commit

Permalink
Merge branch 'main' into feat/#110-explore-page-header
Browse files Browse the repository at this point in the history
  • Loading branch information
VanishMax committed Nov 25, 2024
2 parents 487041c + 455e6b2 commit 22b930a
Show file tree
Hide file tree
Showing 17 changed files with 395 additions and 200 deletions.
3 changes: 0 additions & 3 deletions app/portfolio/page.ts

This file was deleted.

9 changes: 9 additions & 0 deletions app/portfolio/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { PortfolioPage } from '@/pages/portfolio';
import { headers } from 'next/headers';
import { userAgent } from 'next/server';

export default function Portfolio() {
const headersList = headers();
const { device } = userAgent({ headers: headersList });
return <PortfolioPage isMobile={device.type === 'mobile'} />;
}
10 changes: 5 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,14 @@
"@penumbra-labs/registry": "^12.0.0",
"@penumbra-zone/bech32m": "^10.0.0",
"@penumbra-zone/client": "^21.0.0",
"@penumbra-zone/crypto-web": "^28.0.0",
"@penumbra-zone/crypto-web": "^29.0.1",
"@penumbra-zone/getters": "^20.0.0",
"@penumbra-zone/perspective": "^36.0.0",
"@penumbra-zone/perspective": "^39.0.0",
"@penumbra-zone/protobuf": "^6.3.0",
"@penumbra-zone/transport-dom": "^7.5.0",
"@penumbra-zone/types": "^26.0.0",
"@penumbra-zone/ui": "^13.2.0",
"@penumbra-zone/wasm": "^31.0.0",
"@penumbra-zone/types": "^26.2.1",
"@penumbra-zone/ui": "^13.3.1",
"@penumbra-zone/wasm": "^33.1.0",
"@radix-ui/react-icons": "^1.3.0",
"@rehooks/component-size": "^1.0.3",
"@styled-icons/octicons": "^10.47.0",
Expand Down
136 changes: 78 additions & 58 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

54 changes: 50 additions & 4 deletions src/pages/portfolio/index.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,57 @@
'use client';

import React from 'react';
import { XCircle } from 'lucide-react';
import { Button } from '@penumbra-zone/ui/Button';
import { Text } from '@penumbra-zone/ui/Text';
import { Density } from '@penumbra-zone/ui/Density';

export const PortfolioPage = () => {
interface PortfolioPageProps {
isMobile: boolean;
}

export function PortfolioPage({ isMobile }: PortfolioPageProps): React.ReactNode {
return isMobile ? <MobilePortfolioPage /> : <DesktopPortfolioPage />;
}

function MobilePortfolioPage() {
return (
<section>
<Text h2>Hi!</Text>
<section className='absolute inset-0 h-screen flex flex-col items-center justify-between p-4 gap-3 border-t border-neutral-800'>
<div className='flex flex-col justify-center items-center p-0 gap-4 w-full flex-grow'>
<div className='relative'>
<XCircle className='text-neutral-light w-8 h-8' />
</div>

<Text color={'text.secondary'} align={'center'} small={true}>
This page requires a connection to your wallet, please switch to a desktop device.
</Text>

<Density compact={true}>
{/* Copy Link Button */}
<Button
onClick={() => {
// We discard the promise using void,
// because Button only expects void-returning functions.
void (async () => {
/* Write the current url to clipboard */
const currentUrl = window.location.href;
await navigator.clipboard.writeText(currentUrl);
})();
}}
>
Copy Link
</Button>
</Density>
</div>

{/* Go Back Button */}
<Button>
<Text body>Go Back</Text>
</Button>
</section>
);
};
}

function DesktopPortfolioPage() {
return 'Hi from desktop!';
}
8 changes: 4 additions & 4 deletions src/pages/trade/model/trace.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Trace } from '@/shared/api/server/book/types.ts';
import { round } from '@/shared/utils/numbers/round.ts';
import { round } from '@penumbra-zone/types/round';

export const calculateSpread = (sellOrders: Trace[], buyOrders: Trace[]) => {
if (!sellOrders.length || !buyOrders.length) {
Expand All @@ -21,8 +21,8 @@ export const calculateSpread = (sellOrders: Trace[], buyOrders: Trace[]) => {
const spreadPercentage = (spread / midPrice) * 100;

return {
amount: round(spread, 8),
percentage: round(spreadPercentage, 2),
midPrice: round(midPrice, 8),
amount: round({ value: spread, decimals: 8 }),
percentage: round({ value: spreadPercentage, decimals: 2 }),
midPrice: round({ value: midPrice, decimals: 8 }),
};
};
16 changes: 13 additions & 3 deletions src/pages/trade/model/useSummary.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { useQuery } from '@tanstack/react-query';
import { usePathSymbols } from '@/pages/trade/model/use-path.ts';
import { SummaryResponse } from '@/shared/api/server/summary.ts';
import { DurationWindow } from '@/shared/utils/duration.ts';
import { SummaryDataResponse, SummaryResponse } from '@/shared/api/server/types.ts';
import { useRefetchOnNewBlock } from '@/shared/api/compact-block.ts';

export const useSummary = (window: DurationWindow) => {
const { baseSymbol, quoteSymbol } = usePathSymbols();

return useQuery({
const query = useQuery({
queryKey: ['summary', baseSymbol, quoteSymbol],
retry: 1,
queryFn: async () => {
Expand All @@ -22,7 +23,16 @@ export const useSummary = (window: DurationWindow) => {
if ('error' in jsonRes) {
throw new Error(jsonRes.error);
}
return jsonRes;

if ('noData' in jsonRes) {
return jsonRes;
}

return SummaryDataResponse.fromJson(jsonRes);
},
});

useRefetchOnNewBlock(query);

return query;
};
94 changes: 52 additions & 42 deletions src/pages/trade/ui/summary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ import { ReactNode } from 'react';
import cn from 'clsx';
import { useAutoAnimate } from '@formkit/auto-animate/react';
import { Text } from '@penumbra-zone/ui/Text';
import { AssetIcon } from '@penumbra-zone/ui/AssetIcon';
import { Skeleton } from '@/shared/ui/skeleton';
import { useSummary } from '../model/useSummary';
import { usePathToMetadata } from '../model/use-path';
import { shortify } from '@/shared/utils/numbers/shortify';
import { round } from '@/shared/utils/numbers/round';
import { ValueViewComponent } from '@penumbra-zone/ui/ValueView';
import { round } from '@penumbra-zone/types/round';
import { Density } from '@penumbra-zone/ui/Density';
import { SummaryDataResponse } from '@/shared/api/server/types.ts';

const SummaryCard = ({
title,
Expand Down Expand Up @@ -45,15 +45,6 @@ const SummaryCard = ({

export const Summary = () => {
const { data, isLoading, error } = useSummary('1d');
const { quoteAsset } = usePathToMetadata();

const change24h = data && {
positive: data.price >= data.price_then,
change: round(data.price - data.price_then, 4),
percent: !data.price
? '0'
: round(Math.abs(((data.price - data.price_then) / data.price_then) * 100), 2),
};

if (error) {
return (
Expand All @@ -68,57 +59,76 @@ export const Summary = () => {
return (
<div className='flex flex-wrap items-center gap-x-4 gap-y-2'>
<SummaryCard title='Price' loading={isLoading}>
{data && (
<Text detail color='text.primary'>
{data && 'price' in data ? round({ value: data.price, decimals: 6 }) : '-'}
</Text>
</SummaryCard>
<SummaryCard title='24h Change' loading={isLoading}>
{data && 'noData' in data && (
<Text detail color='text.primary'>
{round(data.price, 6)}
-
</Text>
)}
</SummaryCard>
<SummaryCard title='24h Change' loading={isLoading}>
{change24h && (
<div
className={cn(
'flex items-center gap-1',
change24h.positive ? 'text-success-light' : 'text-destructive-light',
)}
>
<Text detail>{change24h.change}</Text>
{data && 'change' in data && (
<div className={cn('flex items-center gap-1', getColor(data, false))}>
<Text detail>{round({ value: data.change.value, decimals: 6 })}</Text>
<span
className={cn(
'flex h-4 px-1 rounded-full text-success-dark',
change24h.positive ? 'bg-success-light' : 'bg-destructive-light',
)}
className={cn('flex h-4 px-1 rounded-full text-success-dark', getColor(data, true))}
>
<Text detail>
{change24h.positive ? '+' : '-'}
{change24h.percent}%
{getTextSign(data)}
{data.change.percent}%
</Text>
</span>
</div>
)}
</SummaryCard>
<SummaryCard title='24h High' loading={isLoading}>
{/* TODO: After added to DB, show here */}
<Text detail color='text.primary'>
-
{data && 'high' in data ? round({ value: data.high, decimals: 6 }) : '-'}
</Text>
</SummaryCard>
<SummaryCard title='24h Low' loading={isLoading}>
{/* TODO: After added to DB, show here */}
<Text detail color='text.primary'>
-
{data && 'low' in data ? round({ value: data.low, decimals: 6 }) : '-'}
</Text>
</SummaryCard>
<SummaryCard title='24h Volume' loading={isLoading}>
{data && (
<div className='flex items-center gap-1'>
{quoteAsset && <AssetIcon metadata={quoteAsset} size='sm' />}
<Text detail color='text.primary'>
{shortify(data.direct_volume_over_window)}
</Text>
</div>
{data && 'directVolume' in data ? (
<Density compact>
<ValueViewComponent
valueView={data.directVolume}
context='table'
abbreviate
hideSymbol
/>
</Density>
) : (
<Text detail color='text.primary'>
-
</Text>
)}
</SummaryCard>
</div>
);
};

const getTextSign = (res: SummaryDataResponse) => {
if (res.change.sign === 'positive') {
return '+';
}
if (res.change.sign === 'negative') {
return '-';
}
return '';
};

const getColor = (res: SummaryDataResponse, isBg = false): string => {
if (res.change.sign === 'positive') {
return isBg ? 'bg-success-light' : 'text-success-light';
}
if (res.change.sign === 'negative') {
return isBg ? 'bg-destructive-light' : 'text-destructive-light';
}
return isBg ? 'bg-neutral-light' : 'text-neutral-light';
};
3 changes: 2 additions & 1 deletion src/shared/api/server/book/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import { Trace, TraceHash } from '@/shared/api/server/book/types.ts';
import { getAssetIdFromValueView } from '@penumbra-zone/getters/value-view';
import { Value, ValueView } from '@penumbra-zone/protobuf/penumbra/core/asset/v1/asset_pb';
import { getDisplayDenomExponent } from '@penumbra-zone/getters/metadata';
import { formatAmount, removeTrailingZeros } from '@penumbra-zone/types/amount';
import { formatAmount } from '@penumbra-zone/types/amount';
import { removeTrailingZeros } from '@penumbra-zone/types/shortify';

// Used to aggregate equal prices along the same route
const getTraceHash = (trace: Trace): TraceHash => {
Expand Down
49 changes: 41 additions & 8 deletions src/shared/api/server/summary.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { NextRequest, NextResponse } from 'next/server';
import { pindexer } from '@/shared/database';
import { ChainRegistryClient } from '@penumbra-labs/registry';
import { DexExPairsSummary } from '@/shared/database/schema.ts';
import { durationWindows, isDurationWindow } from '@/shared/utils/duration.ts';

export type SummaryResponse = DexExPairsSummary | { error: string };
import { ChangeData, SummaryDataResponse, SummaryResponse } from '@/shared/api/server/types.ts';
import { round } from '@penumbra-zone/types/round';
import { toValueView } from '@/shared/utils/value-view.ts';
import { calculateDisplayPrice } from '@/shared/utils/price-conversion.ts';

export async function GET(req: NextRequest): Promise<NextResponse<SummaryResponse>> {
const chainId = process.env['PENUMBRA_CHAIN_ID'];
Expand Down Expand Up @@ -56,11 +57,43 @@ export async function GET(req: NextRequest): Promise<NextResponse<SummaryRespons

const summary = results[0];
if (!summary) {
return NextResponse.json(
{ error: `No summary found for ${baseAssetSymbol}/${quoteAssetSymbol}` },
{ status: 400 },
);
return NextResponse.json({ window: durationWindow, noData: true });
}

return NextResponse.json(summary);
const priceDiff = summary.price - summary.price_then;
const change = {
value: calculateDisplayPrice(priceDiff, baseAssetMetadata, quoteAssetMetadata),
sign: priceDiffLabel(priceDiff),
percent:
summary.price === 0
? '0'
: round({
value: Math.abs(((summary.price - summary.price_then) / summary.price_then) * 100),
decimals: 2,
}),
};

const dataResponse = new SummaryDataResponse({
window: durationWindow,
directVolume: toValueView({
amount: summary.direct_volume_over_window,
metadata: quoteAssetMetadata,
}),
price: calculateDisplayPrice(summary.price, baseAssetMetadata, quoteAssetMetadata),
low: calculateDisplayPrice(summary.low, baseAssetMetadata, quoteAssetMetadata),
high: calculateDisplayPrice(summary.high, baseAssetMetadata, quoteAssetMetadata),
change,
});

return NextResponse.json(dataResponse.toJson());
}

const priceDiffLabel = (num: number): ChangeData['sign'] => {
if (num === 0) {
return 'neutral';
}
if (num > 0) {
return 'positive';
}
return 'negative';
};
Loading

0 comments on commit 22b930a

Please sign in to comment.