"use client"; import React from "react"; import { useEffect, useState, useCallback } from "react"; import { Permission } from "../auth-context"; import { api } from "../api"; import { Header } from "../components/Header"; import { useRequireAuth } from "../hooks/useRequireAuth"; import { components } from "../generated/api"; import { formatDateTime } from "../utils/date"; import { layoutStyles, typographyStyles, bannerStyles, badgeStyles, buttonStyles, } from "../styles/shared"; type ExchangeResponse = components["schemas"]["ExchangeResponse"]; /** * Format EUR amount from cents to display string */ function formatEur(cents: number): string { return `€${(cents / 100).toLocaleString("de-DE")}`; } /** * Format satoshi amount with thousand separators */ function formatSats(sats: number): string { const btc = sats / 100_000_000; const btcStr = btc.toFixed(8); const [whole, decimal] = btcStr.split("."); const grouped = decimal.replace(/(.{2})(.{3})(.{3})/, "$1 $2 $3"); return `${whole}.${grouped}`; } /** * Get status display properties */ function getTradeStatusDisplay(status: string): { text: string; bgColor: string; textColor: string; } { switch (status) { case "booked": return { text: "Pending", bgColor: "rgba(99, 102, 241, 0.2)", textColor: "rgba(129, 140, 248, 0.9)", }; case "completed": return { text: "Completed", bgColor: "rgba(34, 197, 94, 0.2)", textColor: "rgba(34, 197, 94, 0.9)", }; case "cancelled_by_user": return { text: "Cancelled", bgColor: "rgba(239, 68, 68, 0.2)", textColor: "rgba(239, 68, 68, 0.9)", }; case "cancelled_by_admin": return { text: "Cancelled by Admin", bgColor: "rgba(239, 68, 68, 0.2)", textColor: "rgba(239, 68, 68, 0.9)", }; case "no_show": return { text: "No Show", bgColor: "rgba(251, 146, 60, 0.2)", textColor: "rgba(251, 146, 60, 0.9)", }; default: return { text: status, bgColor: "rgba(255, 255, 255, 0.1)", textColor: "rgba(255, 255, 255, 0.6)", }; } } export default function TradesPage() { const { user, isLoading, isAuthorized } = useRequireAuth({ requiredPermission: Permission.VIEW_OWN_APPOINTMENTS, fallbackRedirect: "/", }); const [trades, setTrades] = useState([]); const [isLoadingTrades, setIsLoadingTrades] = useState(true); const [cancellingId, setCancellingId] = useState(null); const [confirmCancelId, setConfirmCancelId] = useState(null); const [error, setError] = useState(null); const fetchTrades = useCallback(async () => { try { const data = await api.get("/api/trades"); setTrades(data); } catch (err) { console.error("Failed to fetch trades:", err); setError("Failed to load trades"); } finally { setIsLoadingTrades(false); } }, []); useEffect(() => { if (user && isAuthorized) { fetchTrades(); } }, [user, isAuthorized, fetchTrades]); const handleCancel = async (tradeId: number) => { setCancellingId(tradeId); setError(null); try { await api.post(`/api/trades/${tradeId}/cancel`, {}); await fetchTrades(); setConfirmCancelId(null); } catch (err) { setError(err instanceof Error ? err.message : "Failed to cancel trade"); } finally { setCancellingId(null); } }; if (isLoading) { return (
Loading...
); } if (!isAuthorized) { return null; } const upcomingTrades = trades.filter( (t) => t.status === "booked" && new Date(t.slot_start) > new Date() ); const pastOrFinalTrades = trades.filter( (t) => t.status !== "booked" || new Date(t.slot_start) <= new Date() ); return (

My Trades

View and manage your Bitcoin trades

{error &&
{error}
} {isLoadingTrades ? (
Loading trades...
) : trades.length === 0 ? (

You don't have any trades yet.

Start trading
) : ( <> {/* Upcoming Trades */} {upcomingTrades.length > 0 && (

Upcoming ({upcomingTrades.length})

{upcomingTrades.map((trade) => { const status = getTradeStatusDisplay(trade.status); const isBuy = trade.direction === "buy"; return (
{formatDateTime(trade.slot_start)}
{isBuy ? "BUY" : "SELL"} {formatEur(trade.eur_amount)} {formatSats(trade.sats_amount)} sats
Rate: € {trade.agreed_price_eur.toLocaleString("de-DE", { maximumFractionDigits: 0, })} /BTC {isBuy ? "+" : "-"} {trade.premium_percentage}%
{status.text}
{trade.status === "booked" && (
{confirmCancelId === trade.id ? ( <> ) : ( )}
)}
); })}
)} {/* Past/Completed/Cancelled Trades */} {pastOrFinalTrades.length > 0 && (

History ({pastOrFinalTrades.length})

{pastOrFinalTrades.map((trade) => { const status = getTradeStatusDisplay(trade.status); const isBuy = trade.direction === "buy"; return (
{formatDateTime(trade.slot_start)}
{isBuy ? "BUY" : "SELL"} {formatEur(trade.eur_amount)} {formatSats(trade.sats_amount)} sats
{status.text}
); })}
)} )}
); } // Page-specific styles const styles: Record = { content: { flex: 1, padding: "2rem", maxWidth: "800px", margin: "0 auto", width: "100%", }, section: { marginBottom: "2rem", }, sectionTitle: { fontFamily: "'DM Sans', system-ui, sans-serif", fontSize: "1.1rem", fontWeight: 500, color: "#fff", marginBottom: "1rem", }, tradeList: { display: "flex", flexDirection: "column", gap: "0.75rem", }, tradeCard: { background: "rgba(255, 255, 255, 0.03)", border: "1px solid rgba(255, 255, 255, 0.08)", borderRadius: "12px", padding: "1.25rem", transition: "all 0.2s", }, tradeCardPast: { opacity: 0.6, background: "rgba(255, 255, 255, 0.01)", }, tradeHeader: { display: "flex", justifyContent: "space-between", alignItems: "flex-start", gap: "1rem", }, tradeInfo: { display: "flex", flexDirection: "column", gap: "0.25rem", }, tradeTime: { fontFamily: "'DM Sans', system-ui, sans-serif", fontSize: "1rem", fontWeight: 500, color: "#fff", marginBottom: "0.5rem", }, tradeDetails: { display: "flex", alignItems: "center", gap: "0.5rem", flexWrap: "wrap", }, directionBadge: { fontFamily: "'DM Sans', system-ui, sans-serif", fontSize: "0.7rem", fontWeight: 600, padding: "0.2rem 0.5rem", borderRadius: "4px", textTransform: "uppercase", }, amount: { fontFamily: "'DM Mono', monospace", fontSize: "0.95rem", color: "#fff", fontWeight: 500, }, arrow: { color: "rgba(255, 255, 255, 0.3)", fontSize: "0.8rem", }, satsAmount: { fontFamily: "'DM Mono', monospace", fontSize: "0.9rem", color: "#f7931a", // Bitcoin orange }, rateRow: { display: "flex", alignItems: "center", gap: "0.5rem", marginTop: "0.25rem", }, rateLabel: { fontFamily: "'DM Sans', system-ui, sans-serif", fontSize: "0.75rem", color: "rgba(255, 255, 255, 0.4)", }, rateValue: { fontFamily: "'DM Mono', monospace", fontSize: "0.8rem", color: "rgba(255, 255, 255, 0.7)", }, premiumBadge: { fontFamily: "'DM Sans', system-ui, sans-serif", fontSize: "0.65rem", fontWeight: 600, padding: "0.15rem 0.4rem", borderRadius: "3px", }, buttonGroup: { display: "flex", gap: "0.5rem", }, confirmButton: { fontFamily: "'DM Sans', system-ui, sans-serif", padding: "0.35rem 0.75rem", fontSize: "0.75rem", background: "rgba(239, 68, 68, 0.2)", border: "1px solid rgba(239, 68, 68, 0.3)", borderRadius: "6px", color: "#f87171", cursor: "pointer", transition: "all 0.2s", }, emptyState: { fontFamily: "'DM Sans', system-ui, sans-serif", color: "rgba(255, 255, 255, 0.4)", textAlign: "center", padding: "3rem", }, emptyStateLink: { color: "#a78bfa", textDecoration: "none", }, };