"use client"; import { useEffect, useMemo, useState } from "react"; import { AlertTriangle, ArrowUpDown, BarChart3, Check, Copy, Loader2, RefreshCcw, } from "lucide-react"; import { CurrencyIcon } from "@/components/converter/currency-icon"; import { CurrencySelect } from "@/components/converter/currency-select"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Separator } from "@/components/ui/separator"; import { Skeleton } from "@/components/ui/skeleton"; import { useCryptoMarket } from "@/hooks/use-crypto-market"; import { useDebouncedValue } from "@/hooks/use-debounced-value"; import { getDisplaySymbol } from "@/lib/currency-display"; import { useMarketRates } from "@/hooks/use-market-rates"; import { formatAmount, formatInverseRate, formatRate, formatSignedPercent, formatTimestamp, formatUsdCompact, formatUsdPrice, } from "@/lib/format"; import { buildRateMap, convertAmount } from "@/lib/rates"; import { cn } from "@/lib/utils"; import { validateAmount } from "@/lib/validation"; const DEFAULT_FROM = "USD"; const DEFAULT_TO = "EUR"; const QUICK_AMOUNTS = [10, 50, 100, 500, 1000] as const; interface ConverterCardProps { forcedFromCode?: string; forcedToCode?: string; onPairChange?: (fromCode: string, toCode: string) => void; } function ConverterSkeleton() { return (
); } function ErrorState({ message, onRetry, }: { message: string; onRetry: () => void; }) { return (

Unable to load market rates

{message}

); } function EmptyState() { return (

No assets are currently available. Please try again shortly.

); } export function ConverterCard({ forcedFromCode, forcedToCode, onPairChange, }: ConverterCardProps) { const { data, error, isLoading, refresh } = useMarketRates(); const [amountInput, setAmountInput] = useState("1"); const [fromCode, setFromCode] = useState(DEFAULT_FROM); const [toCode, setToCode] = useState(DEFAULT_TO); const [isCopied, setIsCopied] = useState(false); const debouncedAmount = useDebouncedValue(amountInput, 120); const assets = useMemo(() => data?.assets ?? [], [data]); const rateMap = useMemo(() => buildRateMap(assets), [assets]); useEffect(() => { if (!assets.length) { return; } if (!rateMap.has(fromCode)) { setFromCode(rateMap.has(DEFAULT_FROM) ? DEFAULT_FROM : assets[0].code); } if (!rateMap.has(toCode)) { const fallback = rateMap.has(DEFAULT_TO) ? DEFAULT_TO : (assets.find((asset) => asset.code !== fromCode)?.code ?? assets[0].code); setToCode(fallback); } }, [assets, fromCode, toCode, rateMap]); useEffect(() => { if (!assets.length) { return; } if (forcedFromCode && rateMap.has(forcedFromCode)) { setFromCode(forcedFromCode); } if (forcedToCode && rateMap.has(forcedToCode)) { setToCode(forcedToCode); } }, [assets, forcedFromCode, forcedToCode, rateMap]); useEffect(() => { onPairChange?.(fromCode, toCode); }, [fromCode, toCode, onPairChange]); const inputValidation = validateAmount(amountInput); const debouncedValidation = validateAmount(debouncedAmount); const fromAsset = rateMap.get(fromCode); const toAsset = rateMap.get(toCode); const convertedValue = useMemo(() => { if (!fromAsset || !toAsset || !debouncedValidation.ok) { return null; } return convertAmount(debouncedValidation.value, fromAsset, toAsset); }, [fromAsset, toAsset, debouncedValidation]); const currentRate = useMemo(() => { if (!fromAsset || !toAsset) { return null; } return formatRate(fromAsset, toAsset); }, [fromAsset, toAsset]); const inverseRate = useMemo(() => { if (!fromAsset || !toAsset) { return null; } return formatInverseRate(fromAsset, toAsset); }, [fromAsset, toAsset]); const handleSwap = () => { setFromCode(toCode); setToCode(fromCode); }; const marketAsset = useMemo(() => { if (toAsset?.type === "crypto") { return toAsset; } if (fromAsset?.type === "crypto") { return fromAsset; } return null; }, [fromAsset, toAsset]); const { data: marketData, error: marketError, isLoading: isMarketLoading, refresh: refreshMarket, } = useCryptoMarket(marketAsset?.code ?? null); const handleCopyConvertedValue = async () => { if (convertedValue === null || !toAsset) { return; } const text = `${formatAmount(convertedValue, toAsset)} ${toAsset.code}`; try { await navigator.clipboard.writeText(text); setIsCopied(true); window.setTimeout(() => setIsCopied(false), 1400); } catch { setIsCopied(false); } }; if (isLoading && !data) { return ; } if (!data && error) { return void refresh()} />; } if (!data || assets.length === 0) { return ; } const convertedDisplay = convertedValue !== null && toAsset ? `${formatAmount(convertedValue, toAsset)} ${toAsset.code}` : "--"; const amountError = inputValidation.ok ? null : inputValidation.error; const amountPrefix = getDisplaySymbol(fromAsset); return ( Convert fiat currencies and cryptocurrencies using live exchange rates. {error ? (
Using last successful data. Latest refresh failed: {error}
) : null}
{amountPrefix ? ( {amountPrefix} ) : null} setAmountInput(event.target.value)} placeholder="Enter amount" className={cn( "h-14 rounded-xl bg-background/70 px-4 text-lg", amountPrefix ? "pl-10" : "", )} aria-invalid={Boolean(amountError)} aria-describedby={amountError ? "amount-error" : undefined} />
{QUICK_AMOUNTS.map((quickAmount) => { const isActive = inputValidation.ok && inputValidation.value === quickAmount; return ( ); })}
{amountError ? (

{amountError}

) : null}

Converted value

{fromAsset && toAsset ? (
{fromAsset.code} to {toAsset.code}
) : null}

{convertedDisplay}

{fromAsset ? (

for{" "} {inputValidation.ok ? formatAmount(inputValidation.value, fromAsset) : "-"}{" "} {fromAsset.code}

) : null}

Current rate

{fromAsset && toAsset && currentRate ? `1 ${fromAsset.code} = ${currentRate} ${toAsset.code}` : "-"}

Inverse rate

{fromAsset && toAsset && inverseRate ? `1 ${toAsset.code} = ${inverseRate} ${fromAsset.code}` : "-"}

Last updated

{formatTimestamp(data.updatedAt)}

{marketAsset ? (

Market data

{marketAsset.code}
{marketError && !marketData ? (

Unable to load market data right now.

) : null}

Price

{marketData ? formatUsdPrice(marketData.priceUsd) : "-"}

24h

0 ? "text-emerald-300" : marketData.change24hPct < 0 ? "text-red-300" : "text-foreground" : "text-foreground", )} > {marketData ? formatSignedPercent(marketData.change24hPct) : "-"}

Market cap

{marketData ? formatUsdCompact(marketData.marketCapUsd) : "-"}

Volume (24h)

{marketData ? formatUsdCompact(marketData.volume24hUsd) : "-"}

{marketData ? `Updated ${formatTimestamp(marketData.updatedAt)}` : isMarketLoading ? "Updating market data..." : ""} {marketData ? Source: {marketData.source} : null}
) : null}
{isLoading ? (
Updating market rates...
) : null}
); }