151 lines
5.3 KiB
TypeScript
151 lines
5.3 KiB
TypeScript
"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>
|
|
);
|
|
}
|