Skip to content
Open
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
81 changes: 15 additions & 66 deletions frontend/README.md
Original file line number Diff line number Diff line change
@@ -1,73 +1,22 @@
# React + TypeScript + Vite
# Frontend

This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
React + TypeScript client for the exchange WebSocket API.

Currently, two official plugins are available:
## Dev server

- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs)
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)

## React Compiler

The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).

## Expanding the ESLint configuration

If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:

```js
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...

// Remove tseslint.configs.recommended and replace with this
tseslint.configs.recommendedTypeChecked,
// Alternatively, use this for stricter rules
tseslint.configs.strictTypeChecked,
// Optionally, add this for stylistic rules
tseslint.configs.stylisticTypeChecked,

// Other configs...
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
```bash
npm install
npm run dev
```

You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
Open the URL Vite prints (usually `http://localhost:5173`).

```js
// eslint.config.js
import reactX from 'eslint-plugin-react-x'
import reactDom from 'eslint-plugin-react-dom'
## Connecting to the backend

export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// Enable lint rules for React
reactX.configs['recommended-typescript'],
// Enable lint rules for React DOM
reactDom.configs.recommended,
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
```
1. Start the C++ backend on port `7000` with SSL certs (see `backend/README.md`).
2. In the UI, connect to `wss://localhost:7000`.
3. Log in with a demo user (`shashwat` / `shashwat` or `nero` / `nero`).
4. Subscribe to `market/snapshot` to stream live bid/ask levels.

Browsers reject self-signed certificates by default. Trust `backend/certs/cert.pem`
locally, or accept the certificate warning before the WebSocket handshake succeeds.
2 changes: 1 addition & 1 deletion frontend/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>frontend</title>
<title>Exchange</title>
</head>
<body>
<div id="root"></div>
Expand Down
218 changes: 215 additions & 3 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,217 @@
function App() {
return <h1>Hello world!</h1>;
import { useState, type FormEvent } from 'react';
import { useExchangeSocket } from './hooks/useExchangeSocket';
import { DEFAULT_WS_URL, type BookLevels } from './types';

const LEVELS = [
'level_1',
'level_2',
'level_3',
'level_4',
'level_5',
] as const;

function formatPrice(n: number): string {
return n.toFixed(6);
}

export default App;
function BookSide({
title,
levels,
side,
}: {
title: string;
levels: BookLevels | undefined;
side: 'ask' | 'bid';
}) {
return (
<div className={`book-side book-side--${side}`}>
<h3>{title}</h3>
<table>
<thead>
<tr>
<th>Level</th>
<th>Price</th>
</tr>
</thead>
<tbody>
{LEVELS.map((key, i) => (
<tr key={key}>
<td>{i + 1}</td>
<td className="mono">
{levels ? formatPrice(levels[key]) : '—'}
</td>
</tr>
))}
</tbody>
</table>
</div>
);
}

function statusLabel(status: string): string {
return status.replaceAll('_', ' ');
}

export default function App() {
const [wsUrl, setWsUrl] = useState(DEFAULT_WS_URL);
const [user, setUser] = useState('shashwat');
const [password, setPassword] = useState('shashwat');

const {
connectionStatus,
authStatus,
username,
snapshot,
subscribed,
lastError,
log,
connect,
disconnect,
login,
subscribe,
unsubscribe,
} = useExchangeSocket(wsUrl);

const onLogin = (e: FormEvent) => {
e.preventDefault();
login(user, password);
};

const connected = connectionStatus === 'connected';
const authenticated = authStatus === 'authenticated';

return (
<div className="app">
<header className="header">
<div>
<h1>Exchange</h1>
<p className="subtitle">Live market data over WSS</p>
</div>
<div className="status-pills">
<span className={`pill pill--${connectionStatus}`}>
{statusLabel(connectionStatus)}
</span>
<span className={`pill pill--${authStatus}`}>
{statusLabel(authStatus)}
{username ? ` · ${username}` : ''}
</span>
</div>
</header>

{lastError && <div className="banner banner--error">{lastError}</div>}

<section className="panel">
<h2>Connection</h2>
<div className="row">
<label className="field grow">
<span>WebSocket URL</span>
<input
value={wsUrl}
onChange={(e) => setWsUrl(e.target.value)}
disabled={connected || connectionStatus === 'connecting'}
spellCheck={false}
/>
</label>
{!connected ? (
<button
type="button"
className="btn btn--primary"
onClick={connect}
disabled={connectionStatus === 'connecting'}
>
{connectionStatus === 'connecting' ? 'Connecting…' : 'Connect'}
</button>
) : (
<button type="button" className="btn" onClick={disconnect}>
Disconnect
</button>
)}
</div>
<p className="hint">
Backend listens on <code>wss://localhost:7000</code>. Self-signed certs
must be trusted by the browser (import <code>backend/certs/cert.pem</code>
or accept the certificate warning).
</p>
</section>

<section className="panel">
<h2>Login</h2>
<form className="row" onSubmit={onLogin}>
<label className="field">
<span>Username</span>
<input
value={user}
onChange={(e) => setUser(e.target.value)}
disabled={!connected || authenticated}
autoComplete="username"
/>
</label>
<label className="field">
<span>Password</span>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
disabled={!connected || authenticated}
autoComplete="current-password"
/>
</label>
<button
type="submit"
className="btn btn--primary"
disabled={!connected || authenticated || authStatus === 'logging_in'}
>
{authStatus === 'logging_in' ? 'Logging in…' : 'Login'}
</button>
</form>
<p className="hint">
Demo users: <code>shashwat / shashwat</code>, <code>nero / nero</code>
</p>
</section>

<section className="panel">
<div className="row between">
<h2>Market Snapshot</h2>
<div className="row gap">
<button
type="button"
className="btn btn--primary"
onClick={subscribe}
disabled={!authenticated || subscribed}
>
Subscribe
</button>
<button
type="button"
className="btn"
onClick={unsubscribe}
disabled={!authenticated || !subscribed}
>
Unsubscribe
</button>
</div>
</div>

<div className="book">
<BookSide title="Bids" levels={snapshot?.bid} side="bid" />
<BookSide title="Asks" levels={snapshot?.ask} side="ask" />
</div>
{!subscribed && authenticated && (
<p className="hint">Subscribe to receive live snapshots every second.</p>
)}
</section>

<section className="panel">
<h2>Event Log</h2>
<ul className="log">
{log.length === 0 && <li className="muted">No events yet</li>}
{log.map((line, i) => (
<li key={`${i}-${line}`} className="mono">
{line}
</li>
))}
</ul>
</section>
</div>
);
}
Loading