refactor(converter): split converter card into modular section components
This commit is contained in:
150
components/converter/converter-card-market-data.tsx
Normal file
150
components/converter/converter-card-market-data.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user