feat(market): add 24h crypto sparkline chart with CoinGecko history and resilient fallback
This commit is contained in:
@@ -13,6 +13,11 @@ export interface CryptoMarketSnapshot {
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface CryptoPricePoint {
|
||||
timestamp: string;
|
||||
priceUsd: number;
|
||||
}
|
||||
|
||||
const COINGECKO_BASE_URL = "https://api.coingecko.com/api/v3";
|
||||
|
||||
function buildCoinGeckoHeaders(): HeadersInit {
|
||||
@@ -91,6 +96,43 @@ export async function fetchCryptoMarketSnapshot(
|
||||
};
|
||||
}
|
||||
|
||||
export async function fetchCryptoPriceHistory24h(
|
||||
providerId: string,
|
||||
): Promise<CryptoPricePoint[]> {
|
||||
const url = `${COINGECKO_BASE_URL}/coins/${encodeURIComponent(
|
||||
providerId,
|
||||
)}/market_chart?vs_currency=usd&days=1`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
next: { revalidate: 60 },
|
||||
headers: buildCoinGeckoHeaders(),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Unable to load crypto price history (${response.status})`);
|
||||
}
|
||||
|
||||
const payload = (await response.json()) as {
|
||||
prices?: Array<[number, number]>;
|
||||
};
|
||||
|
||||
const points = (payload.prices ?? [])
|
||||
.filter(
|
||||
(entry): entry is [number, number] =>
|
||||
Array.isArray(entry) &&
|
||||
entry.length >= 2 &&
|
||||
Number.isFinite(entry[0]) &&
|
||||
Number.isFinite(entry[1]) &&
|
||||
entry[1] > 0,
|
||||
)
|
||||
.map(([timestamp, priceUsd]) => ({
|
||||
timestamp: new Date(timestamp).toISOString(),
|
||||
priceUsd,
|
||||
}));
|
||||
|
||||
return points;
|
||||
}
|
||||
|
||||
export async function fetchCryptoData(): Promise<CryptoRateResult> {
|
||||
const ids = CRYPTO_ASSETS.map((asset) => asset.providerId).filter(
|
||||
(id): id is string => Boolean(id)
|
||||
|
||||
@@ -7,6 +7,10 @@ export interface CryptoMarketResponse {
|
||||
change24hPct: number | null;
|
||||
marketCapUsd: number | null;
|
||||
volume24hUsd: number | null;
|
||||
priceHistory24h: Array<{
|
||||
timestamp: string;
|
||||
priceUsd: number;
|
||||
}>;
|
||||
updatedAt: string;
|
||||
source: string;
|
||||
}
|
||||
@@ -18,6 +22,15 @@ const marketResponseSchema = z.object({
|
||||
change24hPct: z.number().nullable(),
|
||||
marketCapUsd: z.number().nullable(),
|
||||
volume24hUsd: z.number().nullable(),
|
||||
priceHistory24h: z
|
||||
.array(
|
||||
z.object({
|
||||
timestamp: z.string(),
|
||||
priceUsd: z.number().positive(),
|
||||
}),
|
||||
)
|
||||
.optional()
|
||||
.default([]),
|
||||
updatedAt: z.string(),
|
||||
source: z.string(),
|
||||
});
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { fetchCryptoMarketSnapshot } from "@/lib/api/crypto";
|
||||
import {
|
||||
fetchCryptoMarketSnapshot,
|
||||
fetchCryptoPriceHistory24h,
|
||||
} from "@/lib/api/crypto";
|
||||
|
||||
export interface CachedCryptoMarketSnapshot {
|
||||
code: string;
|
||||
@@ -7,6 +10,10 @@ export interface CachedCryptoMarketSnapshot {
|
||||
change24hPct: number | null;
|
||||
marketCapUsd: number | null;
|
||||
volume24hUsd: number | null;
|
||||
priceHistory24h: Array<{
|
||||
timestamp: string;
|
||||
priceUsd: number;
|
||||
}>;
|
||||
updatedAt: string;
|
||||
source: "CoinGecko";
|
||||
}
|
||||
@@ -44,6 +51,13 @@ export async function getCryptoMarketWithCache(params: {
|
||||
|
||||
const requestPromise = (async () => {
|
||||
const snapshot = await fetchCryptoMarketSnapshot(params.providerId);
|
||||
let history: Array<{ timestamp: string; priceUsd: number }> = [];
|
||||
|
||||
try {
|
||||
history = await fetchCryptoPriceHistory24h(params.providerId);
|
||||
} catch {
|
||||
history = [];
|
||||
}
|
||||
|
||||
const normalized: CachedCryptoMarketSnapshot = {
|
||||
code: params.code,
|
||||
@@ -52,6 +66,7 @@ export async function getCryptoMarketWithCache(params: {
|
||||
change24hPct: snapshot.change24hPct,
|
||||
marketCapUsd: snapshot.marketCapUsd,
|
||||
volume24hUsd: snapshot.volume24hUsd,
|
||||
priceHistory24h: history,
|
||||
updatedAt: snapshot.updatedAt,
|
||||
source: "CoinGecko",
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user