arbret/frontend/app/exchange/components/ConfirmationStep.tsx
counterweight 6d0f125536
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.
2025-12-25 19:11:23 +01:00

252 lines
7.3 KiB
TypeScript

"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>
</>
);
}