Skip to content

Commit 1d0ed7e

Browse files
committed
feat: add MEV and gas refund metrics widget with feature flag support
- Add MevMetrics component to display both MEV and gas refunds - Implement feature flag support via API response - Configure via docusaurus.config.js customFields - Entire widget clickable, redirects to protect.flashbots.net - No fallback values - widget hides on error - Gracefully handle loading and error states - Mobile responsive with proper navbar integration
1 parent 6015f3b commit 1d0ed7e

File tree

7 files changed

+421
-1
lines changed

7 files changed

+421
-1
lines changed

MEV_METRICS_IMPLEMENTATION.md

Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
# Adding MEV Metrics Tickers to Docusaurus Navbar
2+
3+
This guide explains how to add MEV metrics tickers to a Docusaurus website navbar using the ComponentTypes pattern.
4+
5+
## Overview
6+
7+
Add a compact metrics ticker to the navbar displaying: `Refund | MEV: 380.290 ETH | Gas: 444.240 ETH`
8+
9+
## Implementation Steps
10+
11+
### 1. Install Dependencies
12+
13+
```bash
14+
npm install
15+
```
16+
17+
Create `.env` file if needed:
18+
```
19+
BASE_URL=/
20+
TARGET_URL=http://localhost:3000
21+
```
22+
23+
### 2. Create MEV Metrics Component
24+
25+
Create `src/components/MevMetrics.tsx`:
26+
27+
```tsx
28+
import React, { useEffect, useState } from 'react';
29+
import styles from './MevMetrics.module.css';
30+
31+
interface MetricData {
32+
value: number;
33+
unit: string;
34+
}
35+
36+
interface MetricsResponse {
37+
mevRefund: MetricData;
38+
gasRefund: MetricData;
39+
}
40+
41+
export default function MevMetrics(): JSX.Element {
42+
const [data, setData] = useState<MetricsResponse | null>(null);
43+
const [loading, setLoading] = useState(true);
44+
45+
useEffect(() => {
46+
const fetchMetrics = async () => {
47+
try {
48+
const response = await fetch('/api/mev/metrics');
49+
const metrics: MetricsResponse = await response.json();
50+
setData(metrics);
51+
} catch (error) {
52+
console.error('Error fetching MEV metrics:', error);
53+
// Mock data as fallback
54+
setData({
55+
mevRefund: { value: 380.29, unit: 'ETH' },
56+
gasRefund: { value: 444.24, unit: 'ETH' }
57+
});
58+
} finally {
59+
setLoading(false);
60+
}
61+
};
62+
63+
fetchMetrics();
64+
}, []);
65+
66+
const formatValue = (metric: MetricData): string => {
67+
if (metric.unit === '$') {
68+
if (metric.value >= 1e9) return `$${(metric.value / 1e9).toFixed(1)}B`;
69+
if (metric.value >= 1e6) return `$${(metric.value / 1e6).toFixed(1)}M`;
70+
if (metric.value >= 1e3) return `$${(metric.value / 1e3).toFixed(1)}K`;
71+
return `$${metric.value.toFixed(2)}`;
72+
}
73+
return `${metric.value.toFixed(3)} ${metric.unit}`;
74+
};
75+
76+
return (
77+
<div className={styles.container}>
78+
<span className={styles.label}>Refund</span>
79+
<span className={styles.separator}>|</span>
80+
<div className={styles.metric}>
81+
<span className={styles.label}>MEV:</span>
82+
<span className={`${styles.value} ${loading ? styles.loading : ''}`}>
83+
{loading ? '...' : data && formatValue(data.mevRefund)}
84+
</span>
85+
</div>
86+
<span className={styles.separator}>|</span>
87+
<div className={styles.metric}>
88+
<span className={styles.label}>Gas:</span>
89+
<span className={`${styles.value} ${loading ? styles.loading : ''}`}>
90+
{loading ? '...' : data && formatValue(data.gasRefund)}
91+
</span>
92+
</div>
93+
</div>
94+
);
95+
}
96+
```
97+
98+
### 3. Create CSS Module
99+
100+
Create `src/components/MevMetrics.module.css`:
101+
102+
```css
103+
.container {
104+
display: flex;
105+
align-items: center;
106+
gap: 0.75rem;
107+
font-size: 0.875rem;
108+
color: var(--ifm-navbar-link-color);
109+
margin-right: 0.75rem;
110+
}
111+
112+
.metric {
113+
display: flex;
114+
align-items: center;
115+
gap: 0.25rem;
116+
}
117+
118+
.label {
119+
font-weight: 400;
120+
}
121+
122+
.value {
123+
font-family: monospace;
124+
font-weight: 600;
125+
transition: opacity 0.3s ease;
126+
}
127+
128+
.loading {
129+
opacity: 0.5;
130+
}
131+
132+
.separator {
133+
color: var(--ifm-navbar-link-color);
134+
opacity: 0.3;
135+
}
136+
137+
@media (max-width: 996px) {
138+
.container {
139+
display: none !important;
140+
}
141+
}
142+
```
143+
144+
### 4. Swizzle ComponentTypes
145+
146+
```bash
147+
npm run swizzle @docusaurus/theme-classic NavbarItem/ComponentTypes -- --eject --typescript
148+
```
149+
150+
### 5. Register Component
151+
152+
Update `src/theme/NavbarItem/ComponentTypes.tsx`:
153+
154+
```tsx
155+
// Add import at top
156+
import MevMetrics from '@site/src/components/MevMetrics';
157+
158+
// Add to ComponentTypes object
159+
const ComponentTypes: ComponentTypesObject = {
160+
// ... existing types
161+
'custom-mevMetrics': MevMetrics,
162+
};
163+
```
164+
165+
### 6. Configure Navbar
166+
167+
Update `docusaurus.config.js`:
168+
169+
```javascript
170+
navbar: {
171+
items: [
172+
{ to: '/docs', label: 'Docs', position: 'left' },
173+
{ to: '/blog', label: 'Blog', position: 'left' },
174+
{
175+
type: 'custom-mevMetrics',
176+
position: 'right',
177+
},
178+
{
179+
href: 'https://your-forum-link.com',
180+
label: 'Forum',
181+
position: 'right',
182+
},
183+
],
184+
},
185+
```
186+
187+
## API Specification
188+
189+
Endpoint: `/api/mev/metrics`
190+
191+
Expected response:
192+
```json
193+
{
194+
"mevRefund": {
195+
"value": 380.29,
196+
"unit": "ETH"
197+
},
198+
"gasRefund": {
199+
"value": 444.24,
200+
"unit": "ETH"
201+
}
202+
}
203+
```
204+
205+
## Features
206+
207+
- **Single API call** for both metrics
208+
- **CSS Modules** for better performance and maintainability
209+
- **Responsive design** - hidden on mobile devices
210+
- **Loading states** with visual feedback
211+
- **Error handling** with mock data fallback
212+
- **Number formatting** for large values (K/M/B)
213+
214+
## Notes
215+
216+
- Mock data is included for development
217+
- Metrics update once on page load (suitable for daily updates)
218+
- Uses Docusaurus theme variables for consistent styling

README.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,25 @@ This command generates static content into the `build` directory and can be serv
4848
Create a PR and once merged, Github actions automatically deploy it.
4949

5050
The docs use Vercel for hosting, and deployment is done by Vercel on any merge into the master branch.
51+
52+
## Refund Metrics Widget
53+
54+
This site displays MEV and gas refund metrics in the navbar, fetched from the [Flashbots Refund Metrics API](https://github.com/flashbots/refund-metrics-dune-api).
55+
56+
### Configuration
57+
58+
To configure the widget, edit `docusaurus.config.js`:
59+
60+
```js
61+
customFields: {
62+
refundMetricsApiUrl: 'https://refund-metrics-dune-api.vercel.app',
63+
refundMetricsRedirectUrl: 'https://protect.flashbots.net/',
64+
},
65+
```
66+
67+
- `refundMetricsApiUrl`: The API endpoint for fetching metrics
68+
- `refundMetricsRedirectUrl`: Where to redirect when users click on MEV refunds
69+
70+
The widget implementation is in `src/components/MevMetrics.tsx`. For Flashbots docs, it:
71+
- Shows both MEV and gas refunds
72+
- Clicking on MEV refunds redirects to the configured URL (default: [Flashbots Protect](https://protect.flashbots.net/))

docusaurus.config.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,10 @@ module.exports = async function createConfigAsync() {
6969
sidebarId: 'api',
7070
position: 'left',
7171
},
72+
{
73+
type: 'custom-mevMetrics',
74+
position: 'right',
75+
},
7276
{
7377
href: 'https://github.com/flashbots/docs',
7478
label: 'GitHub',
@@ -116,5 +120,9 @@ module.exports = async function createConfigAsync() {
116120
},
117121
'docusaurus-plugin-sass'
118122
],
123+
customFields: {
124+
refundMetricsApiUrl: 'https://refund-metrics-dune-api.vercel.app',
125+
refundMetricsRedirectUrl: 'https://protect.flashbots.net/',
126+
},
119127
}
120128
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
.container {
2+
display: flex;
3+
align-items: center;
4+
gap: 0.75rem;
5+
font-size: 0.875rem;
6+
color: var(--ifm-navbar-link-color);
7+
margin-right: 0.75rem;
8+
}
9+
10+
.metric {
11+
display: flex;
12+
align-items: center;
13+
gap: 0.25rem;
14+
}
15+
16+
.label {
17+
font-weight: 400;
18+
}
19+
20+
.value {
21+
font-family: monospace;
22+
font-weight: 600;
23+
transition: opacity 0.3s ease;
24+
}
25+
26+
.loading {
27+
opacity: 0.5;
28+
}
29+
30+
.separator {
31+
color: var(--ifm-navbar-link-color);
32+
opacity: 0.3;
33+
}
34+
35+
.clickable {
36+
cursor: pointer;
37+
transition: opacity 0.2s ease;
38+
}
39+
40+
.clickable:hover {
41+
opacity: 0.8;
42+
}
43+
44+
.clickable:active {
45+
opacity: 0.6;
46+
}
47+
48+
@media (max-width: 996px) {
49+
.container {
50+
display: none !important;
51+
}
52+
}

0 commit comments

Comments
 (0)