-
- Slippage Tolerance
-
- {isCustomSlippage ? customSlippage || '0' : slippage}%
-
-
-
- {[0.1, 0.2, 0.3, 1, 2, 3].map((percent) => (
-
{
- setSlippage(percent);
- setIsCustomSlippage(false);
- }}
- className="flex-1 min-w-[60px]"
- >
- {percent}%
-
- ))}
-
-
{
- setCustomSlippage(e.target.value);
- setIsCustomSlippage(true);
- }}
- onFocus={() => setIsCustomSlippage(true)}
- className={`h-9 pr-6 ${isCustomSlippage ? 'border-primary' : ''}`}
- step="0.01"
- />
- {isCustomSlippage && (
-
- %
+ {/* Gas Estimate, Slippage, and Fees */}
+ {isConnected && fromAmount && toAmount && (
+
+
+ Estimated Gas Cost:
+
+ {isEstimatingGas ? 'Calculating...' : gasEstimate ? `$${gasEstimate.estimatedCostUsd}` : '-'}
+
+
+
+ Max Slippage:
+ {maxSlippage}%
+
+ {swapAmounts && swapAmounts.length > 0 && selectedToToken && (
+
+ Fee:
+
+ {(Number(swapAmounts[0].fee) / Math.pow(10, selectedToToken.decimals)).toFixed(6)} {selectedToToken.symbol}
- )}
-
-
-
-
- {/* Gas Estimate */}
- {isConnected && fromAmount && toAmount && gasEstimate && !isEstimatingGas && (
-
-
- Estimated Gas Cost:
- ${gasEstimate.estimatedCostUsd}
-
-
- )}
- {isConnected && fromAmount && toAmount && isEstimatingGas && (
-
-
- Estimated Gas Cost:
- Calculating...
-
+
+ )}
)}
@@ -356,6 +358,54 @@ export function SwapForm() {
+ {/* Settings Modal */}
+ {isSettingsOpen && (
+ <>
+
setIsSettingsOpen(false)}
+ />
+
+
+
+
Settings
+ setIsSettingsOpen(false)}
+ className="rounded-sm opacity-70 hover:opacity-100 transition-opacity"
+ >
+
+
+
+
+
+
+
+ Max Slippage
+
+
+ setMaxSlippage(e.target.value)}
+ className="pr-8"
+ step="0.1"
+ min="0"
+ max="100"
+ />
+
+ %
+
+
+
+ Your transaction will revert if the price changes unfavorably by more than this percentage.
+
+
+
+
+
+ >
+ )}
+
{/* Review Modal */}
0 ? swapAmounts[0].fee : null}
onConfirm={async () => {
await handleSwap();
setIsReviewModalOpen(false);
diff --git a/src/components/swap-review-modal.tsx b/src/components/swap-review-modal.tsx
index 6a4aec9..3746bf4 100644
--- a/src/components/swap-review-modal.tsx
+++ b/src/components/swap-review-modal.tsx
@@ -4,6 +4,7 @@ import { Button } from '@/components/ui/button';
import { ArrowDown, X } from 'lucide-react';
import type { TokenDetails } from '@/hooks/usePartyPlanner';
import type { GasEstimate } from '@/hooks/usePartyPool';
+import { useToast } from '@/components/ui/toast';
interface SwapReviewModalProps {
open: boolean;
@@ -14,6 +15,7 @@ interface SwapReviewModalProps {
toAmount: string;
slippage: number;
gasEstimate: GasEstimate | null;
+ fee: bigint | null;
onConfirm: () => void;
isSwapping: boolean;
}
@@ -27,11 +29,22 @@ export function SwapReviewModal({
toAmount,
slippage,
gasEstimate,
+ fee,
onConfirm,
isSwapping,
}: SwapReviewModalProps) {
if (!open) return null;
+ // Calculate exchange rate
+ const exchangeRate = fromAmount && toAmount && parseFloat(fromAmount) > 0
+ ? (parseFloat(toAmount) / parseFloat(fromAmount)).toFixed(6)
+ : '0';
+
+ // Format fee
+ const feeFormatted = fee && toToken
+ ? (Number(fee) / Math.pow(10, toToken.decimals)).toFixed(6)
+ : '0';
+
return (
<>
{/* Backdrop */}
@@ -80,11 +93,27 @@ export function SwapReviewModal({
{/* Details */}
+ {/* Exchange Rate */}
+
+ Rate
+
+ 1 {fromToken?.symbol} = {exchangeRate} {toToken?.symbol}
+
+
+
+ {/* Fee */}
+
+ Fee
+ {feeFormatted} {toToken?.symbol}
+
+
+ {/* Slippage */}
Slippage Tolerance
{slippage}%
+ {/* Network Fee */}
{gasEstimate && (
Network Fee
@@ -93,13 +122,13 @@ export function SwapReviewModal({
)}
- {/* Confirm Button */}
+ {/* Approve and Swap Button */}
- {isSwapping ? 'Swapping...' : 'Confirm Swap'}
+ {isSwapping ? 'Swapping...' : 'Approve and Swap'}
diff --git a/src/components/ui/toast.tsx b/src/components/ui/toast.tsx
new file mode 100644
index 0000000..3405acd
--- /dev/null
+++ b/src/components/ui/toast.tsx
@@ -0,0 +1,102 @@
+'use client';
+
+import { createContext, useContext, useState, useCallback, ReactNode } from 'react';
+import { X, Loader2, CheckCircle2 } from 'lucide-react';
+
+interface Toast {
+ id: string;
+ title: string;
+ description?: string;
+ type?: 'loading' | 'success' | 'error';
+}
+
+interface ToastContextType {
+ toasts: Toast[];
+ addToast: (toast: Omit
) => string;
+ removeToast: (id: string) => void;
+ updateToast: (id: string, toast: Partial>) => void;
+}
+
+const ToastContext = createContext(undefined);
+
+export function ToastProvider({ children }: { children: ReactNode }) {
+ const [toasts, setToasts] = useState([]);
+
+ const addToast = useCallback((toast: Omit) => {
+ const id = Math.random().toString(36).substr(2, 9);
+ setToasts((prev) => [...prev, { ...toast, id }]);
+
+ // Auto remove after 5 seconds for success/error toasts
+ if (toast.type === 'success' || toast.type === 'error') {
+ setTimeout(() => {
+ removeToast(id);
+ }, 5000);
+ }
+
+ return id;
+ }, []);
+
+ const removeToast = useCallback((id: string) => {
+ setToasts((prev) => prev.filter((toast) => toast.id !== id));
+ }, []);
+
+ const updateToast = useCallback((id: string, updates: Partial>) => {
+ setToasts((prev) =>
+ prev.map((toast) =>
+ toast.id === id ? { ...toast, ...updates } : toast
+ )
+ );
+
+ // Auto remove if updated to success/error
+ if (updates.type === 'success' || updates.type === 'error') {
+ setTimeout(() => {
+ removeToast(id);
+ }, 5000);
+ }
+ }, [removeToast]);
+
+ return (
+
+ {children}
+
+ {toasts.map((toast) => (
+
+ {toast.type === 'loading' && (
+
+ )}
+ {toast.type === 'success' && (
+
+ )}
+
+
+
{toast.title}
+ {toast.description && (
+
+ {toast.description}
+
+ )}
+
+
+
removeToast(toast.id)}
+ className="rounded-sm opacity-70 hover:opacity-100 transition-opacity"
+ >
+
+
+
+ ))}
+
+
+ );
+}
+
+export function useToast() {
+ const context = useContext(ToastContext);
+ if (!context) {
+ throw new Error('useToast must be used within ToastProvider');
+ }
+ return context;
+}
\ No newline at end of file