Skip to content

Commit

Permalink
feat: #111: trading pairs part 3 (#186)
Browse files Browse the repository at this point in the history
* feat: #111: make stats request in client components

* feat: #111: implement serialize, deserialize and innerFetch helpers

* refactor: use `apiFetch` and serializers for summary and summaries

* fix: use apifetch

* feat: #111: implement pagination of the trading pairs
  • Loading branch information
VanishMax authored Dec 5, 2024
1 parent fa4b98d commit 32f5512
Show file tree
Hide file tree
Showing 10 changed files with 161 additions and 198 deletions.
38 changes: 17 additions & 21 deletions src/pages/explore/api/use-summaries.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,29 @@
'use client';

import { useQuery } from '@tanstack/react-query';
import { SummaryDataResponse } from '@/shared/api/server/summary/types';
import { SummariesResponse } from '@/shared/api/server/summary/all';
import { InfiniteData, QueryKey, useInfiniteQuery } from '@tanstack/react-query';
import { SummaryData } from '@/shared/api/server/summary/types';
import { DurationWindow } from '@/shared/utils/duration';
import { apiFetch } from '@/shared/utils/api-fetch';

const BASE_LIMIT = 15;
const BASE_OFFSET = 0;
const BASE_PAGE = 0;
const BASE_WINDOW: DurationWindow = '1d';

export const useSummaries = () => {
return useQuery({
queryKey: ['summaries', BASE_LIMIT, BASE_OFFSET],
queryFn: async () => {
const paramsObj = {
export const useSummaries = (search: string) => {
return useInfiniteQuery<SummaryData[], Error, InfiniteData<SummaryData[]>, QueryKey, number>({
queryKey: ['summaries', search],
staleTime: 1000 * 60 * 5,
initialPageParam: BASE_PAGE,
getNextPageParam: (lastPage, _, lastPageParam) => {
return lastPage.length ? lastPageParam + 1 : undefined;
},
queryFn: async ({ pageParam }) => {
return apiFetch<SummaryData[]>('/api/summaries', {
search,
limit: BASE_LIMIT.toString(),
offset: BASE_OFFSET.toString(),
offset: (pageParam * BASE_LIMIT).toString(),
durationWindow: BASE_WINDOW,
};

const baseUrl = '/api/summaries';
const urlParams = new URLSearchParams(paramsObj).toString();
const fetchRes = await fetch(`${baseUrl}?${urlParams}`);
const jsonRes = (await fetchRes.json()) as SummariesResponse;
if ('error' in jsonRes) {
throw new Error(jsonRes.error);
}

return jsonRes.map(res => SummaryDataResponse.fromJson(res));
});
},
});
};
8 changes: 4 additions & 4 deletions src/pages/explore/ui/pair-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { Text } from '@penumbra-zone/ui/Text';
import { Density } from '@penumbra-zone/ui/Density';
import { AssetIcon } from '@penumbra-zone/ui/AssetIcon';

import { SummaryDataResponse } from '@/shared/api/server/summary/types';
import { SummaryData } from '@/shared/api/server/summary/types';
import { Skeleton } from '@/shared/ui/skeleton';
import SparklineChart from './sparkline-chart.svg';
import ChevronDown from './chevron-down.svg';
Expand All @@ -31,7 +31,7 @@ const ShimmeringBars = () => {
);
};

const getTextSign = (summary: SummaryDataResponse): ReactNode => {
const getTextSign = (summary: SummaryData): ReactNode => {
if (summary.change.sign === 'positive') {
return <ChevronDown className='size-3 rotate-180 inline-block' />;
}
Expand All @@ -41,7 +41,7 @@ const getTextSign = (summary: SummaryDataResponse): ReactNode => {
return null;
};

const getColor = (summary: SummaryDataResponse): string => {
const getColor = (summary: SummaryData): string => {
if (summary.change.sign === 'positive') {
return 'text-success-light';
}
Expand All @@ -58,7 +58,7 @@ export type PairCardProps =
}
| {
loading: false;
summary: SummaryDataResponse;
summary: SummaryData;
};

export const PairCard = ({ loading, summary }: PairCardProps) => {
Expand Down
72 changes: 62 additions & 10 deletions src/pages/explore/ui/pairs.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,62 @@
'use client';

import { useState } from 'react';
import { useEffect, useRef, useState } from 'react';
import { useAutoAnimate } from '@formkit/auto-animate/react';
import { Search } from 'lucide-react';
import { Text } from '@penumbra-zone/ui/Text';
import { TextInput } from '@penumbra-zone/ui/TextInput';
import { Icon } from '@penumbra-zone/ui/Icon';
import { PairCard } from '@/pages/explore/ui/pair-card';
import { useSummaries } from '@/pages/explore/api/use-summaries';
import { useAutoAnimate } from '@formkit/auto-animate/react';
import SpinnerIcon from '@/shared/assets/spinner-icon.svg';

/** A hook that fires the callback when observed element (on the bottom of the page) is in the view */
const useObserver = (disabled: boolean, cb: VoidFunction) => {
const observerEl = useRef<HTMLDivElement | null>(null);

useEffect(() => {
const ref = observerEl.current;
const observer = new IntersectionObserver(
entries => {
const [entry] = entries;
if (entry?.isIntersecting && !disabled) {
cb();
}
},
{
root: null,
rootMargin: '20px',
threshold: 1.0,
},
);

if (ref) {
observer.observe(ref);
}

return () => {
if (ref) {
observer.unobserve(ref);
}
};
}, [cb, disabled]);

return {
observerEl,
};
};

export const ExplorePairs = () => {
const [parent] = useAutoAnimate();
const { data, isLoading } = useSummaries();

const [search, setSearch] = useState('');

const { data, isLoading, isRefetching, isFetchingNextPage, fetchNextPage } = useSummaries(search);

const { observerEl } = useObserver(isLoading || isRefetching || isFetchingNextPage, () => {
void fetchNextPage();
});

return (
<div className='w-full flex flex-col gap-4'>
<div className='flex gap-4 justify-between items-center text-text-primary'>
Expand Down Expand Up @@ -58,14 +100,24 @@ export const ExplorePairs = () => {
</>
)}

{data?.map(summary => (
<PairCard
loading={false}
summary={summary}
key={`${summary.baseAsset.symbol}/${summary.quoteAsset.symbol}`}
/>
))}
{data?.pages.map(page =>
page.map(summary => (
<PairCard
loading={false}
summary={summary}
key={`${summary.baseAsset.symbol}/${summary.quoteAsset.symbol}`}
/>
)),
)}
</div>

{isFetchingNextPage && (
<div className='flex items-center justify-center h-6 my-1'>
<SpinnerIcon className='animate-spin' />
</div>
)}

<div className='h-1 w-full' ref={observerEl} />
</div>
);
};
20 changes: 4 additions & 16 deletions src/pages/trade/model/useSummary.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { useQuery } from '@tanstack/react-query';
import { usePathSymbols } from '@/pages/trade/model/use-path.ts';
import { DurationWindow } from '@/shared/utils/duration.ts';
import { SummaryDataResponse, SummaryResponse } from '@/shared/api/server/summary/types.ts';
import { NoSummaryData, SummaryData } from '@/shared/api/server/summary/types.ts';
import { useRefetchOnNewBlock } from '@/shared/api/compact-block.ts';
import { apiFetch } from '@/shared/utils/api-fetch';

export const useSummary = (window: DurationWindow) => {
const { baseSymbol, quoteSymbol } = usePathSymbols();
Expand All @@ -11,24 +12,11 @@ export const useSummary = (window: DurationWindow) => {
queryKey: ['summary', baseSymbol, quoteSymbol],
retry: 1,
queryFn: async () => {
const paramsObj = {
return apiFetch<SummaryData | NoSummaryData>('/api/summary', {
durationWindow: window,
baseAsset: baseSymbol,
quoteAsset: quoteSymbol,
};
const baseUrl = '/api/summary';
const urlParams = new URLSearchParams(paramsObj).toString();
const fetchRes = await fetch(`${baseUrl}?${urlParams}`);
const jsonRes = (await fetchRes.json()) as SummaryResponse;
if ('error' in jsonRes) {
throw new Error(jsonRes.error);
}

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

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

Expand Down
6 changes: 3 additions & 3 deletions src/pages/trade/ui/summary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { useSummary } from '../model/useSummary';
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/summary/types.ts';
import { SummaryData } from '@/shared/api/server/summary/types.ts';

const SummaryCard = ({
title,
Expand Down Expand Up @@ -108,7 +108,7 @@ export const Summary = () => {
);
};

const getTextSign = (res: SummaryDataResponse) => {
const getTextSign = (res: SummaryData) => {
if (res.change.sign === 'positive') {
return '+';
}
Expand All @@ -118,7 +118,7 @@ const getTextSign = (res: SummaryDataResponse) => {
return '';
};

const getColor = (res: SummaryDataResponse, isBg = false): string => {
const getColor = (res: SummaryData, isBg = false): string => {
if (res.change.sign === 'positive') {
return isBg ? 'bg-success-light' : 'text-success-light';
}
Expand Down
17 changes: 11 additions & 6 deletions src/shared/api/server/summary/all.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@ import { NextRequest, NextResponse } from 'next/server';
import { pindexer } from '@/shared/database';
import { ChainRegistryClient } from '@penumbra-labs/registry';
import { DurationWindow, durationWindows, isDurationWindow } from '@/shared/utils/duration.ts';
import { SummaryDataResponse, SummaryDataResponseJson } from '@/shared/api/server/summary/types.ts';
import { adaptSummary, SummaryData } from '@/shared/api/server/summary/types.ts';
import { AssetId, Metadata } from '@penumbra-zone/protobuf/penumbra/core/asset/v1/asset_pb';
import { serialize, Serialized } from '@/shared/utils/serializer';

interface GetPairsParams {
window: DurationWindow;
limit: number;
offset: number;
search: string;
}

const getAssetById = (allAssets: Metadata[], id: Buffer): Metadata | undefined => {
Expand All @@ -19,7 +21,7 @@ const getAssetById = (allAssets: Metadata[], id: Buffer): Metadata | undefined =

export const getAllSummaries = async (
params: GetPairsParams,
): Promise<SummaryDataResponseJson[]> => {
): Promise<Serialized<SummaryData>[]> => {
const chainId = process.env['PENUMBRA_CHAIN_ID'];
if (!chainId) {
throw new Error('PENUMBRA_CHAIN_ID is not set');
Expand Down Expand Up @@ -48,27 +50,29 @@ export const getAllSummaries = async (
return undefined;
}

const data = SummaryDataResponse.build(
const data = adaptSummary(
summary,
baseAsset,
quoteAsset,
summary.candles,
summary.candle_times,
);
return data.toJson();

return serialize(data);
}),
);

return summaries.filter(Boolean) as SummaryDataResponseJson[];
return summaries.filter(Boolean) as Serialized<SummaryData>[];
};

export type SummariesResponse = SummaryDataResponseJson[] | { error: string };
export type SummariesResponse = Serialized<SummaryData>[] | { error: string };

export const GET = async (req: NextRequest): Promise<NextResponse<SummariesResponse>> => {
try {
const { searchParams } = new URL(req.url);
const limit = Number(searchParams.get('limit')) || 15;
const offset = Number(searchParams.get('offset')) || 0;
const search = searchParams.get('search') ?? '';
const window = searchParams.get('durationWindow');

if (!window || !isDurationWindow(window)) {
Expand All @@ -84,6 +88,7 @@ export const GET = async (req: NextRequest): Promise<NextResponse<SummariesRespo
window,
limit,
offset,
search,
});

return NextResponse.json(result);
Expand Down
9 changes: 5 additions & 4 deletions src/shared/api/server/summary/single.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ import { NextRequest, NextResponse } from 'next/server';
import { pindexer } from '@/shared/database';
import { ChainRegistryClient } from '@penumbra-labs/registry';
import { durationWindows, isDurationWindow } from '@/shared/utils/duration.ts';
import { SummaryDataResponse, SummaryResponse } from '@/shared/api/server/summary/types.ts';
import { adaptSummary, SummaryResponse } from '@/shared/api/server/summary/types.ts';
import { serialize, Serialized } from '@/shared/utils/serializer';

export async function GET(req: NextRequest): Promise<NextResponse<SummaryResponse>> {
export async function GET(req: NextRequest): Promise<NextResponse<Serialized<SummaryResponse>>> {
const chainId = process.env['PENUMBRA_CHAIN_ID'];
if (!chainId) {
return NextResponse.json({ error: 'PENUMBRA_CHAIN_ID is not set' }, { status: 500 });
Expand Down Expand Up @@ -57,6 +58,6 @@ export async function GET(req: NextRequest): Promise<NextResponse<SummaryRespons
return NextResponse.json({ window: durationWindow, noData: true });
}

const dataResponse = SummaryDataResponse.build(summary, baseAssetMetadata, quoteAssetMetadata);
return NextResponse.json(dataResponse.toJson());
const adapted = adaptSummary(summary, baseAssetMetadata, quoteAssetMetadata);
return NextResponse.json(serialize(adapted));
}
Loading

0 comments on commit 32f5512

Please sign in to comment.