Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: #110: Explore page #133

Draft
wants to merge 6 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/api/stats/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { GET } from '@/shared/api/server/stats';
2 changes: 1 addition & 1 deletion app/explore/page.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
import { ExplorePage } from '@/pages/explore';
import { ExplorePage } from '@/pages/explore/ui/page';

export default ExplorePage;
21 changes: 20 additions & 1 deletion next.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,26 @@ const nextConfig = {
config.module.rules.push({
test: /\.svg$/i,
issuer: /\.[jt]sx?$/,
use: ['@svgr/webpack'],
use: [
{
loader: '@svgr/webpack',
options: {
svgo: false,
svgoConfig: {
plugins: [
{
name: 'preset-default',
params: {
overrides: {
removeViewBox: false,
},
},
},
],
},
},
},
],
});

config.experiments.asyncWebAssembly = true;
Expand Down
22 changes: 22 additions & 0 deletions src/pages/explore/api/use-stats.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { useQuery } from '@tanstack/react-query';
import { StatsResponse, StatsData } from '@/shared/api/server/stats';
import { DurationWindow } from '@/shared/utils/duration';

export const useStats = () => {
return useQuery<StatsData>({
queryKey: ['stats'],
queryFn: async () => {
const baseUrl = '/api/stats';
const urlParams = new URLSearchParams({
durationWindow: '1d' satisfies DurationWindow,
}).toString();

const fetchRes = await fetch(`${baseUrl}?${urlParams}`);
const jsonRes = (await fetchRes.json()) as StatsResponse;
if ('error' in jsonRes) {
throw new Error(jsonRes.error);
}
return jsonRes;
},
});
};
12 changes: 1 addition & 11 deletions src/pages/explore/index.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1 @@
'use client';

import { Text } from '@penumbra-zone/ui/Text';

export const ExplorePage = () => {
return (
<section>
<Text h2>Hi!</Text>
</section>
);
};
export { ExplorePage } from './ui/page';
3 changes: 3 additions & 0 deletions src/pages/explore/ui/chevron-down.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
29 changes: 29 additions & 0 deletions src/pages/explore/ui/info-card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import type { ReactNode } from 'react';
import { useAutoAnimate } from '@formkit/auto-animate/react';
import { Text } from '@penumbra-zone/ui/Text';
import { Skeleton } from '@/shared/ui/skeleton';

export interface InfoCardProps {
title: string;
loading?: boolean;
children: ReactNode;
}

export const InfoCard = ({ title, loading, children }: InfoCardProps) => {
const [parent] = useAutoAnimate();

return (
<div ref={parent} className='flex flex-col justify-center items-start w-full p-3 desktop:p-6 rounded-lg bg-other-tonalFill5 backdrop-blur-lg'>
<Text detail color='text.secondary'>
{title}
</Text>
{loading ? (
<div className="w-14 h-7 py-[6px] px-0">
<Skeleton/>
</div>
) : (
<div className='flex items-baseline justify-start gap-2'>{children}</div>
)}
</div>
);
};
15 changes: 15 additions & 0 deletions src/pages/explore/ui/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
'use client';

import { ExploreStats } from './stats';
import { ExplorePairs } from './pairs';
import PenumbraWaves from './penumbra-waves.svg';

export const ExplorePage = () => {
return (
<section className='flex flex-col gap-6 p-4 max-w-[1062px] mx-auto'>
<PenumbraWaves className='w-screen h-[100vw] -translate-y-[70%] scale-150 fixed -z-[1] pointer-events-none top-0 left-0 desktop:scale-100 desktop:w-[80vw] desktop:h-[80vw] desktop:-translate-y-3/4 desktop:left-[10vw]' />
<ExploreStats />
<ExplorePairs />
</section>
);
};
145 changes: 145 additions & 0 deletions src/pages/explore/ui/pair-card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import { AssetId, Metadata } from '@penumbra-zone/protobuf/penumbra/core/asset/v1/asset_pb';
import { Star, CandlestickChart } from 'lucide-react';
import { Button } from '@penumbra-zone/ui/Button';
import { Text } from '@penumbra-zone/ui/Text';
import { Density } from '@penumbra-zone/ui/Density';
import { AssetIcon } from '@penumbra-zone/ui/AssetIcon';
import SparklineChart from './sparkline-chart.svg';
import { ShortChart } from './short-chart';
import ChevronDown from './chevron-down.svg';

const u8 = (length: number) => Uint8Array.from({ length }, () => Math.floor(Math.random() * 256));
export const PENUMBRA_METADATA = new Metadata({
base: 'upenumbra',
name: 'Penumbra',
display: 'penumbra',
symbol: 'UM',
penumbraAssetId: new AssetId({ inner: u8(32) }),
images: [
{
svg: 'https://raw.githubusercontent.com/prax-wallet/registry/main/images/um.svg',
},
],
});

export const OSMO_METADATA = new Metadata({
symbol: 'OSMO',
name: 'Osmosis',
penumbraAssetId: new AssetId({ inner: u8(32) }),
base: 'uosmo',
display: 'osmo',
denomUnits: [{ denom: 'uosmo' }, { denom: 'osmo', exponent: 6 }],
images: [
{ svg: 'https://raw.githubusercontent.com/prax-wallet/registry/main/images/test-usd.svg ' },
],
});

const ShimmeringBars = () => {
return (
<>
<div className='w-16 h-4 my-1 bg-shimmer rounded-xs' />
<div className='w-10 h-4 bg-shimmer rounded-xs' />
</>
);
};

export interface PairCardProps {
loading?: boolean;
}

export const PairCard = ({ loading }: PairCardProps) => {
const change = -5.35;

return (
<div className='grid grid-cols-subgrid col-span-6 p-3 rounded-sm cursor-pointer transition-colors hover:bg-action-hoverOverlay'>
<div className='relative h-10 flex items-center gap-2 text-text-primary'>
<Density compact>
<Button icon={Star} iconOnly>
Favorite
</Button>
</Density>

<div className='z-10'>
<AssetIcon metadata={PENUMBRA_METADATA} size='lg' />
</div>
<div className='-ml-4'>
<AssetIcon metadata={OSMO_METADATA} size='lg' />
</div>

<Text body>UM/TestUSD</Text>
</div>

<div className='h-10 flex flex-col items-end justify-center'>
{loading ? (
<ShimmeringBars />
) : (
<>
<Text color='text.primary'>0.23</Text>
<Text detail color='text.secondary'>
delUM
</Text>
</>
)}
</div>

<div className='h-10 flex flex-col items-end justify-center'>
{loading ? (
<ShimmeringBars />
) : (
<>
<Text color='text.primary'>2.34M</Text>
<Text detail color='text.secondary'>
USDC
</Text>
</>
)}
</div>

<div className='h-10 flex flex-col items-end justify-center'>
{loading ? (
<ShimmeringBars />
) : (
<>
<Text color='text.primary'>1.37K</Text>
<Text detail color='text.secondary'>
USDC
</Text>
</>
)}
</div>

<div className='h-10 flex items-center justify-end gap-2'>
{loading ? (
<>
<div className="w-10 h-4 bg-shimmer rounded-xs"/>
<SparklineChart className="w-14 h-8"/>
</>
) : (
<>
{change >= 0 ? (

Check failure on line 119 in src/pages/explore/ui/pair-card.tsx

View workflow job for this annotation

GitHub Actions / Lint

Unnecessary conditional, both sides of the expression are literal values
<div className="flex items-center text-success-light">
<ChevronDown className="size-3 rotate-180 inline-block"/>
<Text>{change}%</Text>
</div>
) : (
<div className="flex items-center text-destructive-light">
<ChevronDown className="size-3 inline-block "/>
<Text>{Math.abs(change)}%</Text>
</div>
)}

<ShortChart change={change}/>
</>
)}
</div>

<div className="h-10 flex flex-col items-end justify-center">
<Density compact>
<Button icon={CandlestickChart} iconOnly>
Actions
</Button>
</Density>
</div>
</div>
);
};
52 changes: 52 additions & 0 deletions src/pages/explore/ui/pairs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { useState } from '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';

export const ExplorePairs = () => {
const [search, setSearch] = useState('');

return (
<div className='w-full flex flex-col gap-4'>
<div className='flex gap-4 justify-between items-center text-text-primary'>
<Text large whitespace='nowrap'>
Trading Pairs
</Text>
<TextInput
value={search}
placeholder='Search pair'
startAdornment={<Icon size='md' IconComponent={Search} />}
onChange={setSearch}
/>
</div>

<div className='grid grid-cols-[1fr_1fr_1fr_1fr_128px_56px] gap-2 overflow-auto'>
<div className='grid grid-cols-subgrid col-span-6 py-2 px-3'>
<Text detail color='text.secondary' align='left'>
Pair
</Text>
<Text detail color='text.secondary' align='right'>
Price
</Text>
<Text detail color='text.secondary' align='right'>
Liquidity
</Text>
<Text detail color='text.secondary' align='right' whitespace='nowrap'>
24h Volume
</Text>
<Text detail color='text.secondary' align='right' whitespace='nowrap'>
24h Price Change
</Text>
<Text detail color='text.secondary' align='right'>
Actions
</Text>
</div>

<PairCard />
<PairCard />
</div>
</div>
);
};
15 changes: 15 additions & 0 deletions src/pages/explore/ui/penumbra-waves.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
70 changes: 70 additions & 0 deletions src/pages/explore/ui/short-chart.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
export interface ShortChartProps {
/** Percentage change value. If positive, renders green chart. Otherwise, renders red chart */
change: number;
}

export const ShortChart = ({ change }: ShortChartProps) => {
if (change >= 0) {
return (
<svg
xmlns='http://www.w3.org/2000/svg'
width='58'
height='34'
viewBox='0 0 58 34'
fill='none'
>
<path
opacity='0.32'
d='M27.142 16.6557L12.7666 18.6606C11.6787 18.8124 10.7011 19.4048 10.0631 20.2989L1 33H57V4.25424L45.0452 1.51816C43.6453 1.19777 42.1811 1.65241 41.2089 2.70935L29.5335 15.4021C28.9057 16.0845 28.0604 16.5277 27.142 16.6557Z'
fill='url(#short-chart-gradient)'
/>
<path
d='M57 4.25424L45.0452 1.51816C43.6453 1.19777 42.1811 1.65241 41.2089 2.70935L29.5335 15.4021C28.9057 16.0845 28.0604 16.5277 27.142 16.6557L12.7666 18.6606C11.6787 18.8124 10.7011 19.4048 10.0631 20.2989L1 33'
stroke='#55D383'
strokeLinecap='round'
/>
<defs>
<linearGradient
id='short-chart-gradient'
x1='29'
y1='33'
x2='29'
y2='1'
gradientUnits='userSpaceOnUse'
>
<stop stopColor='#1C793F' stopOpacity='0' />
<stop offset='1' stopColor='#1C793F' />
</linearGradient>
</defs>
</svg>
);
}

return (
<svg xmlns='http://www.w3.org/2000/svg' width='58' height='32' viewBox='0 0 58 32' fill='none'>
<path
opacity='0.32'
d='M1 30.9044L12.6764 25.3016C13.5001 24.9064 14.1637 24.2411 14.5568 23.4164L18.2425 15.6841C18.7342 14.6526 19.6436 13.8806 20.7412 13.5629L32.0101 10.3015L35.4215 6.28357C37.5833 3.73746 41.7289 4.80552 42.3913 8.07922L45.9773 25.8024C46.5597 28.6811 49.9425 29.9714 52.2941 28.2118L57 24.6909V32H1V30.9044Z'
fill='url(#short-chart-red-gradient)'
/>
<path
d='M57 24.6909L52.2941 28.2118C49.9425 29.9714 46.5597 28.6811 45.9773 25.8024L42.3913 8.07922C41.7289 4.80552 37.5833 3.73746 35.4215 6.28357L32.7899 9.38303C32.2814 9.98204 31.6076 10.418 30.8528 10.6364L20.7412 13.5629C19.6436 13.8806 18.7342 14.6526 18.2425 15.6841L14.5568 23.4164C14.1637 24.2411 13.5001 24.9064 12.6764 25.3016L1 30.9044'
stroke='#F17878'
strokeLinecap='round'
/>
<defs>
<linearGradient
id='short-chart-red-gradient'
x1='29'
y1='32'
x2='29'
y2='-2.18182'
gradientUnits='userSpaceOnUse'
>
<stop stopColor='#AF2626' stopOpacity='0' />
<stop offset='1' stopColor='#AF2626' />
</linearGradient>
</defs>
</svg>
);
};
Loading
Loading