Skip to content

Commit

Permalink
docs(nextjs): Add websocket stream to demo
Browse files Browse the repository at this point in the history
  • Loading branch information
ntucker committed Jun 7, 2024
1 parent db19859 commit b8076aa
Show file tree
Hide file tree
Showing 22 changed files with 1,203 additions and 30 deletions.
20 changes: 20 additions & 0 deletions examples/nextjs/app/Provider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
'use client';
import { getDefaultManagers } from '@data-client/react';
import { DataProvider } from '@data-client/ssr/nextjs';
import React from 'react';
import StreamManager from 'resources/StreamManager';
import { getTicker } from 'resources/Ticker';

const managers =
typeof window === 'undefined' ? getDefaultManagers() : (
[
new StreamManager(new WebSocket('wss://ws-feed.exchange.coinbase.com'), {
ticker: getTicker,
}),
...getDefaultManagers(),
]
);

export default function Provider({ children }: { children: React.ReactNode }) {
return <DataProvider managers={managers}>{children}</DataProvider>;
}
24 changes: 24 additions & 0 deletions examples/nextjs/app/crypto/AssetPrice.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { useCache, useSubscription } from '@data-client/react';
import { StatsResource } from 'resources/Stats';
import { getTicker } from 'resources/Ticker';

import { formatPrice } from 'components/formatters';

export default function AssetPrice({ product_id }: Props) {
const price = useLivePrice(product_id);
if (!price) return <span></span>;
return <span>{formatPrice.format(price)}</span>;
}

interface Props {
product_id: string;
}

function useLivePrice(product_id: string) {
useSubscription(getTicker, { product_id });
const ticker = useCache(getTicker, { product_id });
const stats = useCache(StatsResource.get, { id: product_id });
// fallback to stats, as we can load those in a bulk fetch for SSR
// it would be preferable to simply provide bulk fetch of ticker to simplify code here
return ticker?.price ?? stats?.last;
}
57 changes: 57 additions & 0 deletions examples/nextjs/app/crypto/CurrencyList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
'use client';
import Link from 'next/link';
import {
AsyncBoundary,
useFetch,
useQuery,
useSuspense,
} from '@data-client/react';
import { CurrencyResource, queryCurrency } from 'resources/Currency';
import { StatsResource } from 'resources/Stats';

import AssetPrice from './AssetPrice';
import { formatLargePrice } from 'components/formatters';

export default function CurrencyList() {
useFetch(StatsResource.getList);
useSuspense(CurrencyResource.getList);
useSuspense(StatsResource.getList);
const currencies = useQuery(queryCurrency);
if (!currencies) return null;
return (
<table>
<thead>
<tr>
<th></th>
<th align="left">Name</th>
<th>Volume 30d</th>
<th align="right">Price</th>
</tr>
</thead>
<tbody>
{currencies.slice(0, 25).map(currency => (
<tr key={currency.pk()}>
<td>
{currency.icon && (
<img src={currency.icon} width="20" height="20" />
)}
</td>
<td align="left">
<Link href={`/crypto/${currency.id}`}>
{currency.name}
</Link>
</td>
<td align="right">
{formatLargePrice.format(currency?.stats?.volume_usd)}
</td>
<td align="right" width="100">
<AsyncBoundary>
<AssetPrice product_id={`${currency.id}-USD`} />
</AsyncBoundary>
</td>
</tr>
))}
</tbody>
</table>
);
}
18 changes: 18 additions & 0 deletions examples/nextjs/app/crypto/[id]/AssetChart.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
'use client';
import { useLive } from '@data-client/react';
import { lazy } from 'react';
import { getCandles } from 'resources/Candles';

const LineChart = lazy(() => import('./LineChart'));

export default function AssetChart({ product_id, width, height }: Props) {
const candles = useLive(getCandles, { product_id });

return <LineChart data={candles} width={width} height={height} />;
}

interface Props {
product_id: string;
width: number;
height: number;
}
15 changes: 15 additions & 0 deletions examples/nextjs/app/crypto/[id]/AssetPrice.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
'use client';
import { useLive } from '@data-client/react';
import { getTicker } from 'resources/Ticker';

import { formatPrice } from 'components/formatters';

export default function AssetPrice({ product_id }: Props) {
const ticker = useLive(getTicker, { product_id });
const displayPrice = formatPrice.format(ticker.price);
return <span>{displayPrice}</span>;
}

interface Props {
product_id: string;
}
102 changes: 102 additions & 0 deletions examples/nextjs/app/crypto/[id]/LineChart.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { scaleLinear, scaleTime, line, extent, max, min } from 'd3';
import { memo, useMemo } from 'react';

import { formatPrice } from 'components/formatters';

const TICK_LENGTH = 5;
const AXIS_HEIGHT = 20;

function LineChart({ data, width = 500, height = 400 }: Props) {
const graphDetails = {
xScale: scaleTime().range([0, width]),
yScale: scaleLinear().range([height - AXIS_HEIGHT, 0]),
lineGenerator: line<{ timestamp: number; price_open: number }>(),
};
const xRange: [number, number] = extent(
data,
candle => new Date(1000 * candle.timestamp),
) as any;
const yExtent = [
min(data, candle => candle.price_open),
max(data, candle => candle.price_open),
] as [number, number];
graphDetails.xScale.domain(xRange);
graphDetails.yScale.domain(yExtent);
graphDetails.lineGenerator.x((d, i) =>
graphDetails.xScale(new Date(1000 * d.timestamp)),
);
graphDetails.lineGenerator.y(d => graphDetails.yScale(d.price_open));
const path = graphDetails.lineGenerator(data);

const ticks = useMemo(() => {
const pixelsPerTick = 5000;
const width = xRange[1] - xRange[0];
const numberOfTicksTarget = 5;

return graphDetails.xScale.ticks(numberOfTicksTarget).map(value => ({
value,
xOffset: graphDetails.xScale(value),
}));
}, [graphDetails.xScale]);

if (!path) return <div>Failed to generate path</div>;

return (
<svg
viewBox={`0 0 ${width} ${height}`}
width="100%"
height={height}
preserveAspectRatio="none"
style={{ overflow: 'visible' }}
>
<path d={path} fill="none" stroke="currentColor" strokeWidth="2" />
{/* <text
stroke="#fff"
style={{
fontSize: '10px',
textAnchor: 'middle',
transform: 'translate(30px,10px)',
}}
>
{formatPrice.format(yExtent[1])}
</text>
<text
stroke="#fff"
style={{
fontSize: '10px',
textAnchor: 'middle',
transform: `translate(30px,${height - AXIS_HEIGHT}px)`,
}}
>
{formatPrice.format(yExtent[0])}
</text> */}
{ticks.map(({ value, xOffset }, i) => (
<g key={i} transform={`translate(${xOffset}, ${height - AXIS_HEIGHT})`}>
<line y2={TICK_LENGTH} stroke="currentColor" />
<text
stroke="currentColor"
style={{
fontSize: '10px',
textAnchor: 'middle',
transform: 'translateY(20px)',
}}
>
{formatter.format(value)}
</text>
</g>
))}
</svg>
);
}

interface Props {
data: { timestamp: number; price_open: number }[];
width?: number;
height?: number;
}

const formatter = new Intl.DateTimeFormat('en-US', {
timeStyle: 'medium',
});

export default memo(LineChart);
27 changes: 27 additions & 0 deletions examples/nextjs/app/crypto/[id]/Stats.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
'use client'
import { useSuspense } from '@data-client/react';
import { StatsResource } from 'resources/Stats';

import { formatPrice, formatLargePrice } from 'components/formatters';

export default function Stats({ id }: { id: string }) {
const stats = useSuspense(StatsResource.get, { id });
return (
<table>
<tbody>
<tr>
<th align="right">high</th>
<td>{formatPrice.format(stats.high)}</td>
</tr>
<tr>
<th align="right">low</th>
<td>{formatPrice.format(stats.low)}</td>
</tr>
<tr>
<th align="right">volume</th>
<td>{formatLargePrice.format(stats.volume_usd)}</td>
</tr>
</tbody>
</table>
);
}
7 changes: 7 additions & 0 deletions examples/nextjs/app/crypto/[id]/page.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.statSection {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
margin-top: 20px;
}
39 changes: 39 additions & 0 deletions examples/nextjs/app/crypto/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
'use client';
import { AsyncBoundary, useSuspense } from '@data-client/react';
import { CurrencyResource } from 'resources/Currency';

import AssetChart from './AssetChart';
import AssetPrice from './AssetPrice';
import Stats from './Stats';
import styles from './page.module.css'

export default function AssetDetail({ params:{id} }: { params:{id: string} }) {
const currency = useSuspense(CurrencyResource.get, { id });
const width = 600;
const height = 400;

return (
<>
<title>{`${currency.name} Prices with Reactive Data Client`}</title>
<header>
<h1>
<img
src={currency.icon}
style={{ height: '1em', width: '1em', marginBottom: '-.1em' }}
/>{' '}
{currency.name}
</h1>
<h2>
<AssetPrice product_id={`${currency.id}-USD`} />
</h2>
</header>
<AsyncBoundary fallback={<div style={{ width, height }}>&nbsp;</div>}>
<AssetChart product_id={`${currency.id}-USD`} width={width} height={height} />
</AsyncBoundary>
<section className={styles.statSection}>
<Stats id={`${currency.id}-USD`} />
</section>
</>
);
}

32 changes: 32 additions & 0 deletions examples/nextjs/app/crypto/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import Link from 'next/link';

export default function CryptoLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<>
<meta
name="description"
content="Live BTC price using the Reactive Data Client"
/>

{/* <h2 className={styles.subtitle}>
Here we show the live price of BTC using Reactive Data Client
</h2> */}

{children}

<p>
The latest price is immediately available before any JavaScript runs;
while automatically updating as prices change.
</p>

<p>
<Link href="/">Todo List</Link> |{' '}
<Link href="/crypto">Crypto List</Link>
</p>
</>
);
}
27 changes: 2 additions & 25 deletions examples/nextjs/app/crypto/page.tsx
Original file line number Diff line number Diff line change
@@ -1,33 +1,10 @@
import Link from 'next/link';

import AssetPrice from 'components/AssetPrice';
import styles from 'styles/Home.module.css';
import CurrencyList from './CurrencyList';

export default function Crypto() {
return (
<>
<title>Live Crypto Prices with Reactive Data Client</title>
<meta
name="description"
content="Live BTC price using the Reactive Data Client"
/>

<h2 className={styles.subtitle}>
Here we show the live price of BTC using Reactive Data Client
</h2>

<p className={styles.price}>
<AssetPrice symbol="BTC" />
</p>

<p>
The latest price is immediately available before any JavaScript runs;
while automatically updating as prices change.
</p>

<p>
<Link href="/">Todo List</Link>
</p>
<CurrencyList />
</>
);
}
Loading

0 comments on commit b8076aa

Please sign in to comment.