diff --git a/frontend/app/trades/[id]/page.tsx b/frontend/app/trades/[id]/page.tsx new file mode 100644 index 0000000..a5a1cd2 --- /dev/null +++ b/frontend/app/trades/[id]/page.tsx @@ -0,0 +1,300 @@ +"use client"; + +import { useEffect, useState, CSSProperties } from "react"; +import { useParams, useRouter } from "next/navigation"; +import { Permission } from "../../auth-context"; +import { api } from "../../api"; +import { Header } from "../../components/Header"; +import { SatsDisplay } from "../../components/SatsDisplay"; +import { useRequireAuth } from "../../hooks/useRequireAuth"; +import { components } from "../../generated/api"; +import { formatDateTime } from "../../utils/date"; +import { formatEur, getTradeStatusDisplay } from "../../utils/exchange"; +import { + layoutStyles, + typographyStyles, + bannerStyles, + badgeStyles, + buttonStyles, + tradeCardStyles, +} from "../../styles/shared"; + +type ExchangeResponse = components["schemas"]["ExchangeResponse"]; + +export default function TradeDetailPage() { + const router = useRouter(); + const params = useParams(); + const tradeId = params?.id ? parseInt(params.id as string, 10) : null; + + const { user, isLoading, isAuthorized } = useRequireAuth({ + requiredPermission: Permission.VIEW_OWN_EXCHANGES, + fallbackRedirect: "/", + }); + + const [trade, setTrade] = useState(null); + const [isLoadingTrade, setIsLoadingTrade] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + if (!user || !isAuthorized || !tradeId) return; + + const fetchTrade = async () => { + try { + setIsLoadingTrade(true); + setError(null); + const data = await api.get(`/api/trades/${tradeId}`); + setTrade(data); + } catch (err) { + console.error("Failed to fetch trade:", err); + setError( + "Failed to load trade. It may not exist or you may not have permission to view it." + ); + } finally { + setIsLoadingTrade(false); + } + }; + + fetchTrade(); + }, [user, isAuthorized, tradeId]); + + if (isLoading || isLoadingTrade) { + return ( +
+
Loading...
+
+ ); + } + + if (!isAuthorized) { + return null; + } + + if (error || !trade) { + return ( +
+
+
+

Trade Details

+ {error &&
{error}
} + +
+
+ ); + } + + const status = getTradeStatusDisplay(trade.status); + const isBuy = trade.direction === "buy"; + + return ( +
+
+
+
+

Trade Details

+ +
+ +
+
+

Trade Information

+
+
+ Status: + + {status.text} + +
+
+ Time: + {formatDateTime(trade.slot_start)} +
+
+ Direction: + + {isBuy ? "BUY BTC" : "SELL BTC"} + +
+
+ Payment Method: + + {isBuy + ? `Receive via ${trade.bitcoin_transfer_method === "onchain" ? "Onchain" : "Lightning"}` + : `Send via ${trade.bitcoin_transfer_method === "onchain" ? "Onchain" : "Lightning"}`} + +
+
+
+ +
+

Amounts

+
+
+ EUR Amount: + {formatEur(trade.eur_amount)} +
+
+ Bitcoin Amount: + + + +
+
+
+ +
+

Pricing

+
+
+ Market Price: + + € + {trade.market_price_eur.toLocaleString("de-DE", { + maximumFractionDigits: 0, + })} + /BTC + +
+
+ Agreed Price: + + € + {trade.agreed_price_eur.toLocaleString("de-DE", { + maximumFractionDigits: 0, + })} + /BTC + +
+
+ Premium: + {trade.premium_percentage}% +
+
+
+ +
+

Timestamps

+
+
+ Created: + {formatDateTime(trade.created_at)} +
+ {trade.cancelled_at && ( +
+ Cancelled: + {formatDateTime(trade.cancelled_at)} +
+ )} + {trade.completed_at && ( +
+ Completed: + {formatDateTime(trade.completed_at)} +
+ )} +
+
+ + {trade.status === "booked" && ( +
+ +
+ )} +
+
+
+ ); +} + +const styles: Record = { + content: { + flex: 1, + padding: "2rem", + maxWidth: "800px", + margin: "0 auto", + width: "100%", + }, + header: { + display: "flex", + justifyContent: "space-between", + alignItems: "center", + marginBottom: "2rem", + }, + tradeDetailCard: { + background: "rgba(255, 255, 255, 0.03)", + border: "1px solid rgba(255, 255, 255, 0.08)", + borderRadius: "12px", + padding: "2rem", + }, + detailSection: { + marginBottom: "2rem", + }, + sectionTitle: { + fontFamily: "'DM Sans', system-ui, sans-serif", + fontSize: "1.1rem", + fontWeight: 600, + color: "#fff", + marginBottom: "1rem", + }, + detailGrid: { + display: "flex", + flexDirection: "column", + gap: "1rem", + }, + detailRow: { + display: "flex", + justifyContent: "space-between", + alignItems: "center", + padding: "0.75rem 0", + borderBottom: "1px solid rgba(255, 255, 255, 0.05)", + }, + detailLabel: { + fontFamily: "'DM Sans', system-ui, sans-serif", + fontSize: "0.9rem", + color: "rgba(255, 255, 255, 0.6)", + }, + detailValue: { + fontFamily: "'DM Sans', system-ui, sans-serif", + fontSize: "0.9rem", + color: "#fff", + fontWeight: 500, + }, + actionSection: { + marginTop: "2rem", + paddingTop: "2rem", + borderTop: "1px solid rgba(255, 255, 255, 0.1)", + }, +};