refactor(converter): split converter card into modular section components

This commit is contained in:
2026-03-30 19:26:18 +02:00
parent a45393ac00
commit 24f21d25bc
11 changed files with 1075 additions and 755 deletions

View File

@@ -0,0 +1,150 @@
"use client";
import { BarChart3 } from "lucide-react";
import { CurrencyIcon } from "@/components/converter/currency-icon";
import { PriceSparkline } from "@/components/converter/price-sparkline";
import { Button } from "@/components/ui/button";
import { type CryptoMarketResponse, MARKET_CHART_RANGES, type MarketChartRange } from "@/lib/market";
import { formatSignedPercent, formatTimestamp, formatUsdCompact, formatUsdPrice } from "@/lib/format";
import type { RateAsset } from "@/lib/rates";
import { cn } from "@/lib/utils";
interface ConverterCardMarketDataProps {
marketAsset: RateAsset | null;
marketData: CryptoMarketResponse | null;
marketError: string | null;
isMarketLoading: boolean;
marketRange: MarketChartRange;
marketRangeLabels: Record<MarketChartRange, string>;
marketRangeChangePct: number | null;
onMarketRangeChange: (range: MarketChartRange) => void;
}
export function ConverterCardMarketData({
marketAsset,
marketData,
marketError,
isMarketLoading,
marketRange,
marketRangeLabels,
marketRangeChangePct,
onMarketRangeChange,
}: ConverterCardMarketDataProps) {
if (!marketAsset) {
return null;
}
return (
<div className="rounded-xl border border-border/70 bg-background/40 p-4">
<div className="flex items-center justify-between gap-2">
<p className="inline-flex items-center gap-1.5 text-xs uppercase tracking-[0.12em] text-muted-foreground">
<BarChart3 className="h-3.5 w-3.5" />
Market data
</p>
<span className="inline-flex items-center gap-1.5 rounded-full border border-border/70 bg-background/70 px-2 py-1 text-xs text-foreground">
<CurrencyIcon
code={marketAsset.code}
type={marketAsset.type}
size="sm"
/>
{marketAsset.code}
</span>
</div>
<div className="mt-3 flex flex-wrap gap-1.5">
{MARKET_CHART_RANGES.map((range) => {
const isActive = marketRange === range;
return (
<Button
key={range}
type="button"
variant="outline"
size="sm"
onClick={() => onMarketRangeChange(range)}
className={cn(
"h-7 rounded-full border-border/70 px-3 text-xs text-muted-foreground hover:text-foreground",
isActive ? "border-cyan-300/50 bg-cyan-500/15 text-cyan-100" : "",
)}
aria-pressed={isActive}
aria-label={`Show ${marketRangeLabels[range]} chart`}
>
{marketRangeLabels[range]}
</Button>
);
})}
</div>
{marketError && !marketData ? (
<p className="mt-3 text-xs text-red-300/90">
Unable to load market data right now.
</p>
) : null}
<PriceSparkline
points={marketData?.priceHistory ?? []}
rangeLabel={marketRangeLabels[marketRange]}
isLoading={isMarketLoading && !marketData}
className="mt-3"
/>
<div className="mt-3 grid gap-2 sm:grid-cols-2 lg:grid-cols-4">
<div className="rounded-lg border border-border/60 bg-background/60 p-3">
<p className="text-xs uppercase tracking-[0.1em] text-muted-foreground">
Price
</p>
<p className="mt-1 text-base font-medium text-foreground">
{marketData ? formatUsdPrice(marketData.priceUsd) : "-"}
</p>
</div>
<div className="rounded-lg border border-border/60 bg-background/60 p-3">
<p className="text-xs uppercase tracking-[0.1em] text-muted-foreground">
{marketRangeLabels[marketRange]}
</p>
<p
className={cn(
"mt-1 text-base font-medium text-foreground",
marketRangeChangePct !== null
? marketRangeChangePct > 0
? "text-emerald-300"
: marketRangeChangePct < 0
? "text-red-300"
: "text-foreground"
: "text-foreground",
)}
>
{marketData ? formatSignedPercent(marketRangeChangePct) : "-"}
</p>
</div>
<div className="rounded-lg border border-border/60 bg-background/60 p-3">
<p className="text-xs uppercase tracking-[0.1em] text-muted-foreground">
Market cap
</p>
<p className="mt-1 text-base font-medium text-foreground">
{marketData ? formatUsdCompact(marketData.marketCapUsd) : "-"}
</p>
</div>
<div className="rounded-lg border border-border/60 bg-background/60 p-3">
<p className="text-xs uppercase tracking-[0.1em] text-muted-foreground">
Volume (24h)
</p>
<p className="mt-1 text-base font-medium text-foreground">
{marketData ? formatUsdCompact(marketData.volume24hUsd) : "-"}
</p>
</div>
</div>
<div className="mt-3 flex min-h-4 items-center justify-between gap-2 text-xs text-muted-foreground">
<span>
{marketData
? `Updated ${formatTimestamp(marketData.updatedAt)}`
: isMarketLoading
? "Updating market data..."
: ""}
</span>
{marketData ? <span>Source: {marketData.source}</span> : null}
</div>
</div>
);
}