refactor(frontend): break down large Exchange page component
Break down the 1300+ line Exchange page into smaller, focused components: - Create useExchangePrice hook - Handles price fetching and auto-refresh logic - Manages price loading and error states - Centralizes price-related state management - Create useAvailableSlots hook - Manages slot fetching and availability checking - Handles date availability state - Fetches availability when entering booking/confirmation steps - Create PriceDisplay component - Displays market price, agreed price, and premium - Shows price update timestamp and stale warnings - Handles loading and error states - Create ExchangeDetailsStep component - Step 1 of wizard: direction, payment method, amount selection - Contains all form logic for trade details - Validates and displays trade summary - Create BookingStep component - Step 2 of wizard: date and slot selection - Shows trade summary card - Handles date availability and existing trade warnings - Create ConfirmationStep component - Step 3 of wizard: final confirmation - Shows compressed booking summary - Displays all trade details for review - Create StepIndicator component - Visual indicator of current wizard step - Shows completed and active steps - Refactor ExchangePage - Reduced from 1300+ lines to ~350 lines - Uses new hooks and components - Maintains all existing functionality - Improved maintainability and testability All frontend tests pass. Linting passes.
This commit is contained in:
parent
3beb23a765
commit
6d0f125536
8 changed files with 1490 additions and 1055 deletions
252
frontend/app/exchange/components/ConfirmationStep.tsx
Normal file
252
frontend/app/exchange/components/ConfirmationStep.tsx
Normal file
|
|
@ -0,0 +1,252 @@
|
|||
"use client";
|
||||
|
||||
import { CSSProperties } from "react";
|
||||
import { SatsDisplay } from "../../components/SatsDisplay";
|
||||
import { components } from "../../generated/api";
|
||||
import { formatTime } from "../../utils/date";
|
||||
import { formatEur } from "../../utils/exchange";
|
||||
import { buttonStyles } from "../../styles/shared";
|
||||
|
||||
type BookableSlot = components["schemas"]["BookableSlot"];
|
||||
type Direction = "buy" | "sell";
|
||||
type BitcoinTransferMethod = "onchain" | "lightning";
|
||||
|
||||
interface ConfirmationStepProps {
|
||||
selectedSlot: BookableSlot;
|
||||
selectedDate: Date | null;
|
||||
direction: Direction;
|
||||
bitcoinTransferMethod: BitcoinTransferMethod;
|
||||
eurAmount: number;
|
||||
satsAmount: number;
|
||||
agreedPrice: number;
|
||||
isBooking: boolean;
|
||||
isPriceStale: boolean;
|
||||
onConfirm: () => void;
|
||||
onBack: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format price for display
|
||||
*/
|
||||
function formatPrice(price: number): string {
|
||||
return `€${price.toLocaleString("de-DE", { maximumFractionDigits: 0 })}`;
|
||||
}
|
||||
|
||||
const styles: Record<string, CSSProperties> = {
|
||||
confirmCard: {
|
||||
background: "rgba(255, 255, 255, 0.03)",
|
||||
border: "1px solid rgba(255, 255, 255, 0.08)",
|
||||
borderRadius: "12px",
|
||||
padding: "1.5rem",
|
||||
maxWidth: "400px",
|
||||
},
|
||||
confirmTitle: {
|
||||
fontFamily: "'DM Sans', system-ui, sans-serif",
|
||||
fontSize: "1.1rem",
|
||||
fontWeight: 500,
|
||||
color: "#fff",
|
||||
marginBottom: "1rem",
|
||||
},
|
||||
confirmDetails: {
|
||||
marginBottom: "1.5rem",
|
||||
},
|
||||
confirmRow: {
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
padding: "0.5rem 0",
|
||||
borderBottom: "1px solid rgba(255, 255, 255, 0.05)",
|
||||
},
|
||||
confirmLabel: {
|
||||
fontFamily: "'DM Sans', system-ui, sans-serif",
|
||||
color: "rgba(255, 255, 255, 0.5)",
|
||||
fontSize: "0.875rem",
|
||||
},
|
||||
confirmValue: {
|
||||
fontFamily: "'DM Sans', system-ui, sans-serif",
|
||||
color: "#fff",
|
||||
fontSize: "0.875rem",
|
||||
fontWeight: 500,
|
||||
},
|
||||
satsValue: {
|
||||
fontFamily: "'DM Mono', monospace",
|
||||
color: "#f7931a", // Bitcoin orange
|
||||
},
|
||||
buttonRow: {
|
||||
display: "flex",
|
||||
gap: "0.75rem",
|
||||
},
|
||||
bookButton: {
|
||||
fontFamily: "'DM Sans', system-ui, sans-serif",
|
||||
flex: 1,
|
||||
padding: "0.875rem",
|
||||
border: "none",
|
||||
borderRadius: "8px",
|
||||
color: "#fff",
|
||||
fontWeight: 600,
|
||||
cursor: "pointer",
|
||||
transition: "all 0.2s",
|
||||
},
|
||||
cancelButton: {
|
||||
fontFamily: "'DM Sans', system-ui, sans-serif",
|
||||
padding: "0.875rem 1.25rem",
|
||||
background: "rgba(255, 255, 255, 0.05)",
|
||||
border: "1px solid rgba(255, 255, 255, 0.1)",
|
||||
borderRadius: "8px",
|
||||
color: "rgba(255, 255, 255, 0.7)",
|
||||
cursor: "pointer",
|
||||
transition: "all 0.2s",
|
||||
},
|
||||
compressedBookingCard: {
|
||||
background: "rgba(255, 255, 255, 0.03)",
|
||||
border: "1px solid rgba(255, 255, 255, 0.08)",
|
||||
borderRadius: "12px",
|
||||
padding: "1rem 1.5rem",
|
||||
marginBottom: "1.5rem",
|
||||
},
|
||||
compressedBookingHeader: {
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
marginBottom: "0.5rem",
|
||||
},
|
||||
compressedBookingTitle: {
|
||||
fontFamily: "'DM Sans', system-ui, sans-serif",
|
||||
fontSize: "0.875rem",
|
||||
color: "rgba(255, 255, 255, 0.5)",
|
||||
},
|
||||
compressedBookingDetails: {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "0.75rem",
|
||||
flexWrap: "wrap",
|
||||
fontFamily: "'DM Sans', system-ui, sans-serif",
|
||||
fontSize: "1rem",
|
||||
color: "#fff",
|
||||
},
|
||||
summaryDivider: {
|
||||
color: "rgba(255, 255, 255, 0.3)",
|
||||
},
|
||||
editButton: {
|
||||
fontFamily: "'DM Sans', system-ui, sans-serif",
|
||||
fontSize: "0.75rem",
|
||||
color: "#a78bfa",
|
||||
background: "transparent",
|
||||
border: "none",
|
||||
cursor: "pointer",
|
||||
padding: 0,
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Step 3 of the exchange wizard: Confirmation
|
||||
* Shows compressed booking summary and final confirmation form.
|
||||
*/
|
||||
export function ConfirmationStep({
|
||||
selectedSlot,
|
||||
selectedDate,
|
||||
direction,
|
||||
bitcoinTransferMethod,
|
||||
eurAmount,
|
||||
satsAmount,
|
||||
agreedPrice,
|
||||
isBooking,
|
||||
isPriceStale,
|
||||
onConfirm,
|
||||
onBack,
|
||||
}: ConfirmationStepProps) {
|
||||
return (
|
||||
<>
|
||||
{/* Compressed Booking Summary */}
|
||||
<div style={styles.compressedBookingCard}>
|
||||
<div style={styles.compressedBookingHeader}>
|
||||
<span style={styles.compressedBookingTitle}>Appointment</span>
|
||||
<button onClick={onBack} style={styles.editButton}>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<div style={styles.compressedBookingDetails}>
|
||||
<span>
|
||||
{selectedDate?.toLocaleDateString("en-US", {
|
||||
weekday: "short",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
})}
|
||||
</span>
|
||||
<span style={styles.summaryDivider}>•</span>
|
||||
<span>
|
||||
{formatTime(selectedSlot.start_time)} - {formatTime(selectedSlot.end_time)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Confirmation Card */}
|
||||
<div style={styles.confirmCard}>
|
||||
<h3 style={styles.confirmTitle}>Confirm Trade</h3>
|
||||
<div style={styles.confirmDetails}>
|
||||
<div style={styles.confirmRow}>
|
||||
<span style={styles.confirmLabel}>Time:</span>
|
||||
<span style={styles.confirmValue}>
|
||||
{formatTime(selectedSlot.start_time)} - {formatTime(selectedSlot.end_time)}
|
||||
</span>
|
||||
</div>
|
||||
<div style={styles.confirmRow}>
|
||||
<span style={styles.confirmLabel}>Direction:</span>
|
||||
<span
|
||||
style={{
|
||||
...styles.confirmValue,
|
||||
color: direction === "buy" ? "#4ade80" : "#f87171",
|
||||
}}
|
||||
>
|
||||
{direction === "buy" ? "Buy BTC" : "Sell BTC"}
|
||||
</span>
|
||||
</div>
|
||||
<div style={styles.confirmRow}>
|
||||
<span style={styles.confirmLabel}>EUR:</span>
|
||||
<span style={styles.confirmValue}>{formatEur(eurAmount)}</span>
|
||||
</div>
|
||||
<div style={styles.confirmRow}>
|
||||
<span style={styles.confirmLabel}>BTC:</span>
|
||||
<span style={{ ...styles.confirmValue, ...styles.satsValue }}>
|
||||
<SatsDisplay sats={satsAmount} />
|
||||
</span>
|
||||
</div>
|
||||
<div style={styles.confirmRow}>
|
||||
<span style={styles.confirmLabel}>Rate:</span>
|
||||
<span style={styles.confirmValue}>{formatPrice(agreedPrice)}/BTC</span>
|
||||
</div>
|
||||
<div style={styles.confirmRow}>
|
||||
<span style={styles.confirmLabel}>Payment:</span>
|
||||
<span style={styles.confirmValue}>
|
||||
{direction === "buy" ? "Receive via " : "Send via "}
|
||||
{bitcoinTransferMethod === "onchain" ? "Onchain" : "Lightning"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={styles.buttonRow}>
|
||||
<button
|
||||
onClick={onConfirm}
|
||||
disabled={isBooking || isPriceStale}
|
||||
style={{
|
||||
...styles.bookButton,
|
||||
background:
|
||||
direction === "buy"
|
||||
? "linear-gradient(135deg, #4ade80 0%, #22c55e 100%)"
|
||||
: "linear-gradient(135deg, #f87171 0%, #ef4444 100%)",
|
||||
...(isBooking || isPriceStale ? buttonStyles.buttonDisabled : {}),
|
||||
}}
|
||||
>
|
||||
{isBooking
|
||||
? "Booking..."
|
||||
: isPriceStale
|
||||
? "Price Stale"
|
||||
: `Confirm ${direction === "buy" ? "Buy" : "Sell"}`}
|
||||
</button>
|
||||
<button onClick={onBack} disabled={isBooking} style={styles.cancelButton}>
|
||||
Back
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue