Skip to content
Merged
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
140 changes: 140 additions & 0 deletions apps/web/components/BridgeCompare.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
// packages/ui/src/components/BridgeCompare.tsx

import React, { useState, useEffect } from 'react';
import { useBridgeQuotes, BridgeQuoteParams } from '@bridgewise/react';
import { RefreshIndicator } from './RefreshIndicator';
import { QuoteCard } from './QuoteCard';
import { SlippageWarning } from './SlippageWarning';

interface BridgeCompareProps {
initialParams: BridgeQuoteParams;
onQuoteSelect?: (quoteId: string) => void;
refreshInterval?: number;
autoRefresh?: boolean;
}

export const BridgeCompare: React.FC<BridgeCompareProps> = ({
initialParams,
onQuoteSelect,
refreshInterval = 15000,
autoRefresh = true
}) => {
const [selectedQuoteId, setSelectedQuoteId] = useState<string | null>(null);
const [showRefreshIndicator, setShowRefreshIndicator] = useState(false);

const {
quotes,
isLoading,
error,
lastRefreshed,
isRefreshing,
refresh,
updateParams,
retryCount
} = useBridgeQuotes({
initialParams,
intervalMs: refreshInterval,
autoRefresh,
onRefreshStart: () => setShowRefreshIndicator(true),
onRefreshEnd: () => {
setTimeout(() => setShowRefreshIndicator(false), 1000);
}
});

// Handle quote selection
const handleQuoteSelect = (quoteId: string) => {
setSelectedQuoteId(quoteId);
onQuoteSelect?.(quoteId);
};

// Format last refreshed time
const getLastRefreshedText = () => {
if (!lastRefreshed) return 'Never';

const seconds = Math.floor((Date.now() - lastRefreshed.getTime()) / 1000);

if (seconds < 60) return `${seconds}s ago`;
if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`;
return lastRefreshed.toLocaleTimeString();
};

return (
<div className="bridge-compare">
{/* Header with refresh controls */}
<div className="bridge-compare__header">
<h2>Bridge Routes</h2>

<div className="bridge-compare__refresh-controls">
<RefreshIndicator
isRefreshing={isRefreshing}
lastRefreshed={lastRefreshed}
onClick={refresh}
showAnimation={showRefreshIndicator}
/>

<div className="bridge-compare__refresh-info">
<span className="bridge-compare__refresh-time">
Updated: {getLastRefreshedText()}
</span>
{retryCount > 0 && (
<span className="bridge-compare__retry-count">
Retry {retryCount}/{3}
</span>
)}
</div>
</div>
</div>

{/* Error state */}
{error && (
<div className="bridge-compare__error" role="alert">
<p>Failed to fetch quotes: {error.message}</p>
<button onClick={refresh} disabled={isRefreshing}>
Try Again
</button>
</div>
)}

{/* Loading skeleton */}
{isLoading && quotes.length === 0 && (
<div className="bridge-compare__skeleton">
{[1, 2, 3].map((i) => (
<div key={i} className="quote-skeleton" />
))}
</div>
)}

{/* Quotes grid */}
{quotes.length > 0 && (
<div className="bridge-compare__quotes-grid">
{quotes.map((quote) => (
<QuoteCard
key={quote.id}
quote={quote}
isSelected={selectedQuoteId === quote.id}
onSelect={() => handleQuoteSelect(quote.id)}
isRefreshing={isRefreshing && showRefreshIndicator}
/>
))}
</div>
)}

{/* Slippage warning for outdated quotes */}
{lastRefreshed && (
<SlippageWarning
lastRefreshed={lastRefreshed}
quotes={quotes}
refreshThreshold={30000} // 30 seconds
onRefresh={refresh}
/>
)}

{/* Empty state */}
{!isLoading && quotes.length === 0 && !error && (
<div className="bridge-compare__empty">
<p>No bridge routes found for the selected parameters</p>
</div>
)}
</div>
);
};
73 changes: 73 additions & 0 deletions apps/web/components/RefreshIndicator.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import React, { useEffect, useState } from 'react';

interface RefreshIndicatorProps {
isRefreshing: boolean;
lastRefreshed: Date | null;
onClick: () => void;
showAnimation?: boolean;
}

export const RefreshIndicator: React.FC<RefreshIndicatorProps> = ({
isRefreshing,
lastRefreshed,
onClick,
showAnimation = false
}) => {
const [animate, setAnimate] = useState(false);

useEffect(() => {
if (showAnimation) {
setAnimate(true);
const timer = setTimeout(() => setAnimate(false), 1000);
return () => clearTimeout(timer);
}
}, [showAnimation]);

const getTimeSinceLastRefresh = () => {
if (!lastRefreshed) return 'Never';

const seconds = Math.floor((Date.now() - lastRefreshed.getTime()) / 1000);

if (seconds < 60) return `${seconds}s`;
if (seconds < 3600) return `${Math.floor(seconds / 60)}m`;
return `${Math.floor(seconds / 3600)}h`;
};

return (
<button
className={`refresh-indicator ${animate ? 'refresh-indicator--pulse' : ''}`}
onClick={onClick}
disabled={isRefreshing}
aria-label="Refresh quotes"
title={`Last refreshed: ${getTimeSinceLastRefresh()} ago`}
>
<svg
className={`refresh-indicator__icon ${isRefreshing ? 'refresh-indicator__icon--spinning' : ''}`}
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3C15.3019 3 18.1885 4.77814 19.7545 7.42909"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M17 7H21V3"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>

{isRefreshing && (
<span className="refresh-indicator__text">Refreshing...</span>
)}
</button>
);
};
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@

import React from 'react';
import { useTransactionPersistence } from './hooks/useTransactionPersistence';
import { useTransactionPersistence } from './ui-lib/hooks/useTransactionPersistence';

export const TransactionHeartbeat = () => {
const { state, clearState } = useTransactionPersistence();
Expand Down
Loading
Loading