Skip to content

Commit affd7b5

Browse files
committed
Add real-time MEV and gas refund metrics to navbar
- Add comprehensive refund metrics widget showing MEV and gas refund totals - Fetch live data from configurable Dune Analytics API with feature flag support - Use navbar__item class for seamless Docusaurus framework integration - Perfect typography: regular labels, semibold values (font-weight: 500) matching navbar links - Display format: 'Refund | MEV: X.XX ETH | Gas: X.XX ETH' with separators - Include click handler redirecting to Dune analytics dashboard - Responsive design with mobile hide behavior - Robust error handling with graceful degradation Technical implementation: - Multi-metric layout with proper flex alignment and baseline typography - Custom navbar component following Docusaurus integration patterns - Server-side rendering compatible with runtime feature flag control - Minimal CSS leveraging framework styles for consistent appearance
1 parent 6015f3b commit affd7b5

File tree

4 files changed

+171
-0
lines changed

4 files changed

+171
-0
lines changed

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
}

src/components/MevMetrics.module.css

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
.container {
2+
display: flex;
3+
align-items: center;
4+
gap: 0.75rem;
5+
color: var(--ifm-navbar-link-color);
6+
}
7+
8+
.metric {
9+
display: flex;
10+
align-items: baseline;
11+
gap: 0.25rem;
12+
}
13+
14+
.value {
15+
font-weight: 500;
16+
}
17+
18+
.loading {
19+
opacity: 0.5;
20+
}
21+
22+
.separator {
23+
color: var(--ifm-navbar-link-color);
24+
opacity: 0.3;
25+
}
26+
27+
.clickable {
28+
cursor: pointer;
29+
transition: opacity 0.2s ease;
30+
}
31+
32+
.clickable:hover {
33+
opacity: 0.8;
34+
}
35+
36+
.clickable:active {
37+
opacity: 0.6;
38+
}
39+
40+
@media (max-width: 996px) {
41+
.container {
42+
display: none !important;
43+
}
44+
}

src/components/MevMetrics.tsx

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import React, { useEffect, useState } from 'react';
2+
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
3+
import styles from './MevMetrics.module.css';
4+
5+
interface MetricsResponse {
6+
totalMevRefund: number;
7+
totalGasRefund: number;
8+
fetchedAt: string;
9+
stale: boolean;
10+
showWidget?: boolean;
11+
}
12+
13+
export default function MevMetrics(): JSX.Element | null {
14+
const { siteConfig } = useDocusaurusContext();
15+
const [data, setData] = useState<MetricsResponse | null>(null);
16+
const [showWidget, setShowWidget] = useState(true);
17+
const [loading, setLoading] = useState(true);
18+
19+
useEffect(() => {
20+
const fetchMetrics = async () => {
21+
try {
22+
const apiUrl = siteConfig.customFields?.refundMetricsApiUrl as string;
23+
const response = await fetch(`${apiUrl}/api/metrics`);
24+
if (!response.ok) {
25+
throw new Error(`HTTP error! status: ${response.status}`);
26+
}
27+
const metrics: MetricsResponse = await response.json();
28+
29+
// Check feature flag
30+
if (metrics.showWidget === false) {
31+
setShowWidget(false);
32+
setLoading(false);
33+
return;
34+
}
35+
36+
setData(metrics);
37+
} catch (error) {
38+
console.error('Error fetching MEV metrics:', error);
39+
// Don't show widget on error
40+
setData(null);
41+
} finally {
42+
setLoading(false);
43+
}
44+
};
45+
46+
fetchMetrics();
47+
}, []);
48+
49+
const formatValue = (value: number): string => {
50+
return `${value.toFixed(2)} ETH`;
51+
};
52+
53+
const handleClick = () => {
54+
const redirectUrl = siteConfig.customFields?.refundMetricsRedirectUrl as string;
55+
window.open(redirectUrl, '_blank', 'noopener,noreferrer');
56+
};
57+
58+
// Hide widget if flag says so or if no data
59+
if (!showWidget || (!loading && !data)) {
60+
return null;
61+
}
62+
63+
return (
64+
<div
65+
className={`navbar__item ${styles.container} ${styles.clickable}`}
66+
onClick={handleClick}
67+
role="button"
68+
tabIndex={0}
69+
onKeyDown={(e) => {
70+
if (e.key === 'Enter' || e.key === ' ') {
71+
handleClick();
72+
}
73+
}}
74+
>
75+
<span className={styles.label}>Refund</span>
76+
<span className={styles.separator}>|</span>
77+
<div className={styles.metric}>
78+
<span className={styles.label}>MEV:</span>
79+
<span className={`${styles.value} ${loading ? styles.loading : ''}`}>
80+
{loading ? '...' : data && formatValue(data.totalMevRefund)}
81+
</span>
82+
</div>
83+
<span className={styles.separator}>|</span>
84+
<div className={styles.metric}>
85+
<span className={styles.label}>Gas:</span>
86+
<span className={`${styles.value} ${loading ? styles.loading : ''}`}>
87+
{loading ? '...' : data && formatValue(data.totalGasRefund)}
88+
</span>
89+
</div>
90+
</div>
91+
);
92+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import DefaultNavbarItem from '@theme/NavbarItem/DefaultNavbarItem';
2+
import DropdownNavbarItem from '@theme/NavbarItem/DropdownNavbarItem';
3+
import LocaleDropdownNavbarItem from '@theme/NavbarItem/LocaleDropdownNavbarItem';
4+
import SearchNavbarItem from '@theme/NavbarItem/SearchNavbarItem';
5+
import HtmlNavbarItem from '@theme/NavbarItem/HtmlNavbarItem';
6+
import DocNavbarItem from '@theme/NavbarItem/DocNavbarItem';
7+
import DocSidebarNavbarItem from '@theme/NavbarItem/DocSidebarNavbarItem';
8+
import DocsVersionNavbarItem from '@theme/NavbarItem/DocsVersionNavbarItem';
9+
import DocsVersionDropdownNavbarItem from '@theme/NavbarItem/DocsVersionDropdownNavbarItem';
10+
import MevMetrics from '@site/src/components/MevMetrics';
11+
12+
import type {ComponentTypesObject} from '@theme/NavbarItem/ComponentTypes';
13+
14+
const ComponentTypes: ComponentTypesObject = {
15+
default: DefaultNavbarItem,
16+
localeDropdown: LocaleDropdownNavbarItem,
17+
search: SearchNavbarItem,
18+
dropdown: DropdownNavbarItem,
19+
html: HtmlNavbarItem,
20+
doc: DocNavbarItem,
21+
docSidebar: DocSidebarNavbarItem,
22+
docsVersion: DocsVersionNavbarItem,
23+
docsVersionDropdown: DocsVersionDropdownNavbarItem,
24+
'custom-mevMetrics': MevMetrics,
25+
};
26+
27+
export default ComponentTypes;

0 commit comments

Comments
 (0)