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
130
frontend/app/exchange/components/PriceDisplay.tsx
Normal file
130
frontend/app/exchange/components/PriceDisplay.tsx
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
"use client";
|
||||
|
||||
import { CSSProperties } from "react";
|
||||
import { components } from "../../generated/api";
|
||||
|
||||
type ExchangePriceResponse = components["schemas"]["ExchangePriceResponse"];
|
||||
|
||||
interface PriceDisplayProps {
|
||||
priceData: ExchangePriceResponse | null;
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
lastUpdate: Date | null;
|
||||
direction: "buy" | "sell";
|
||||
agreedPrice: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format price for display
|
||||
*/
|
||||
function formatPrice(price: number): string {
|
||||
return `€${price.toLocaleString("de-DE", { maximumFractionDigits: 0 })}`;
|
||||
}
|
||||
|
||||
const styles: Record<string, CSSProperties> = {
|
||||
priceCard: {
|
||||
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",
|
||||
},
|
||||
priceRow: {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "0.75rem",
|
||||
flexWrap: "wrap",
|
||||
},
|
||||
priceLabel: {
|
||||
fontFamily: "'DM Sans', system-ui, sans-serif",
|
||||
color: "rgba(255, 255, 255, 0.5)",
|
||||
fontSize: "0.9rem",
|
||||
},
|
||||
priceValue: {
|
||||
fontFamily: "'DM Mono', monospace",
|
||||
color: "#fff",
|
||||
fontSize: "1.1rem",
|
||||
fontWeight: 500,
|
||||
},
|
||||
priceDivider: {
|
||||
color: "rgba(255, 255, 255, 0.2)",
|
||||
margin: "0 0.25rem",
|
||||
},
|
||||
premiumBadge: {
|
||||
fontFamily: "'DM Sans', system-ui, sans-serif",
|
||||
fontSize: "0.75rem",
|
||||
fontWeight: 600,
|
||||
padding: "0.2rem 0.5rem",
|
||||
borderRadius: "4px",
|
||||
marginLeft: "0.25rem",
|
||||
background: "rgba(255, 255, 255, 0.1)",
|
||||
color: "rgba(255, 255, 255, 0.7)",
|
||||
},
|
||||
priceTimestamp: {
|
||||
fontFamily: "'DM Sans', system-ui, sans-serif",
|
||||
fontSize: "0.75rem",
|
||||
color: "rgba(255, 255, 255, 0.4)",
|
||||
marginTop: "0.5rem",
|
||||
},
|
||||
staleWarning: {
|
||||
color: "#f87171",
|
||||
fontWeight: 600,
|
||||
},
|
||||
priceLoading: {
|
||||
fontFamily: "'DM Sans', system-ui, sans-serif",
|
||||
color: "rgba(255, 255, 255, 0.5)",
|
||||
textAlign: "center" as const,
|
||||
},
|
||||
priceError: {
|
||||
fontFamily: "'DM Sans', system-ui, sans-serif",
|
||||
color: "#f87171",
|
||||
textAlign: "center" as const,
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Component that displays exchange price information.
|
||||
* Shows market price, agreed price, premium percentage, and last update time.
|
||||
*/
|
||||
export function PriceDisplay({
|
||||
priceData,
|
||||
isLoading,
|
||||
error,
|
||||
lastUpdate,
|
||||
direction,
|
||||
agreedPrice,
|
||||
}: PriceDisplayProps) {
|
||||
const marketPrice = priceData?.price?.market_price ?? 0;
|
||||
const premiumPercent = priceData?.price?.premium_percentage ?? 5;
|
||||
const isPriceStale = priceData?.price?.is_stale ?? false;
|
||||
|
||||
return (
|
||||
<div style={styles.priceCard}>
|
||||
{isLoading && !priceData ? (
|
||||
<div style={styles.priceLoading}>Loading price...</div>
|
||||
) : error && !priceData?.price ? (
|
||||
<div style={styles.priceError}>{error}</div>
|
||||
) : (
|
||||
<>
|
||||
<div style={styles.priceRow}>
|
||||
<span style={styles.priceLabel}>Market:</span>
|
||||
<span style={styles.priceValue}>{formatPrice(marketPrice)}</span>
|
||||
<span style={styles.priceDivider}>•</span>
|
||||
<span style={styles.priceLabel}>Our price:</span>
|
||||
<span style={styles.priceValue}>{formatPrice(agreedPrice)}</span>
|
||||
<span style={styles.premiumBadge}>
|
||||
{direction === "buy" ? "+" : "-"}
|
||||
{premiumPercent}%
|
||||
</span>
|
||||
</div>
|
||||
{lastUpdate && (
|
||||
<div style={styles.priceTimestamp}>
|
||||
Updated {lastUpdate.toLocaleTimeString()}
|
||||
{isPriceStale && <span style={styles.staleWarning}> (stale)</span>}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue